v1.9.1
This commit is contained in:
parent
27ce69c2d2
commit
bb3beaa353
12 changed files with 465 additions and 147 deletions
|
|
@ -109,6 +109,21 @@ class Robotstxt_Manager_Updater {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Security patch declared for exactly this installed version
|
||||
// (Core 1.16.0+): offer the patch, never the feature mainline.
|
||||
$patch = self::security_patch_for( $entry, $version );
|
||||
|
||||
if ( '' !== $patch ) {
|
||||
if ( 'premium' === $entry['type'] && ! $this->has_api_key() ) {
|
||||
unset( $updates->response[ $plugin_file ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
$updates->response[ $plugin_file ] = $this->build_update_object( $slug, $plugin_file, $entry, null, $patch );
|
||||
unset( $updates->no_update[ $plugin_file ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( version_compare( $version, $entry['new_version'], '<' ) ) {
|
||||
// Premium updates need the account key in the package URL;
|
||||
// without a usable key the native updater would only hit a
|
||||
|
|
@ -155,6 +170,56 @@ class Robotstxt_Manager_Updater {
|
|||
return '' !== $store_host && $store_host === $entry_host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the security-patch version declared for exactly the given
|
||||
* installed version, or '' when none applies.
|
||||
*
|
||||
* Exact-match by design: a patch declared for 1.2.3 applies only to
|
||||
* sites running 1.2.3 — other versions follow the normal mainline flow.
|
||||
* A declaration that does not strictly increase the version (typo,
|
||||
* downgrade, re-install loop) never counts as a patch.
|
||||
*
|
||||
* @param array<string, mixed> $entry Catalog row (raw or normalised).
|
||||
* @param string $installed_version Version installed on this site.
|
||||
*
|
||||
* @return string Patch version, or ''.
|
||||
*/
|
||||
public static function security_patch_for( array $entry, string $installed_version ): string {
|
||||
if ( '' === $installed_version ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$patches = self::normalize_patches( $entry );
|
||||
$patch = $patches[ $installed_version ] ?? '';
|
||||
|
||||
if ( '' === $patch || ! version_compare( $installed_version, $patch, '<' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises a catalog row's security_patches field into a string map.
|
||||
*
|
||||
* @param array<string, mixed> $row Catalog row.
|
||||
*
|
||||
* @return array<string, string> Installed version => patch version.
|
||||
*/
|
||||
private static function normalize_patches( array $row ): array {
|
||||
$raw = $row['security_patches'] ?? array();
|
||||
$raw = is_array( $raw ) ? $raw : array();
|
||||
$clean = array();
|
||||
|
||||
foreach ( $raw as $applies_to => $patch ) {
|
||||
if ( is_string( $applies_to ) && is_string( $patch ) ) {
|
||||
$clean[ $applies_to ] = $patch;
|
||||
}
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installed plugin versions, from the object-cached plugin list.
|
||||
*
|
||||
|
|
@ -258,7 +323,7 @@ class Robotstxt_Manager_Updater {
|
|||
*
|
||||
* @param list<array<string,mixed>> $catalog Catalog entries from Core.
|
||||
*
|
||||
* @return array<string, array<string,string>> Normalised entries keyed by slug.
|
||||
* @return array<string, array{name:string, type:string, new_version:string, security_patches:array<string,string>, requires_wp:string, requires_php:string, tested_up_to:string, page_url:string, description:string, icon_url:string, banner_url:string}> Normalised entries keyed by slug.
|
||||
*/
|
||||
private function catalog_by_slug( array $catalog ): array {
|
||||
$entries = array();
|
||||
|
|
@ -272,16 +337,17 @@ class Robotstxt_Manager_Updater {
|
|||
}
|
||||
|
||||
$entries[ $slug ] = array(
|
||||
'name' => $this->str( $row, 'name', $slug ),
|
||||
'type' => $this->str( $row, 'type', 'free' ),
|
||||
'new_version' => $this->str( $row, 'current_version', '' ),
|
||||
'requires_wp' => $this->str( $row, 'requires_wp', '' ),
|
||||
'requires_php' => $this->str( $row, 'requires_php', '' ),
|
||||
'tested_up_to' => $this->str( $row, 'tested_up_to', '' ),
|
||||
'page_url' => $this->str( $row, 'page_url', '' ),
|
||||
'description' => $this->localized_description( $row ),
|
||||
'icon_url' => $this->str( $row, 'icon_url', '' ),
|
||||
'banner_url' => $this->str( $row, 'banner_url', '' ),
|
||||
'name' => $this->str( $row, 'name', $slug ),
|
||||
'type' => $this->str( $row, 'type', 'free' ),
|
||||
'new_version' => $this->str( $row, 'current_version', '' ),
|
||||
'security_patches' => self::normalize_patches( $row ),
|
||||
'requires_wp' => $this->str( $row, 'requires_wp', '' ),
|
||||
'requires_php' => $this->str( $row, 'requires_php', '' ),
|
||||
'tested_up_to' => $this->str( $row, 'tested_up_to', '' ),
|
||||
'page_url' => $this->str( $row, 'page_url', '' ),
|
||||
'description' => $this->localized_description( $row ),
|
||||
'icon_url' => $this->str( $row, 'icon_url', '' ),
|
||||
'banner_url' => $this->str( $row, 'banner_url', '' ),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -291,25 +357,29 @@ class Robotstxt_Manager_Updater {
|
|||
/**
|
||||
* Builds the update/no-update object for the WordPress transient.
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @param string $plugin_file Plugin basename.
|
||||
* @param array<string, string> $entry Normalised catalog entry.
|
||||
* @param string|null $current_version Installed version; when null an
|
||||
* update entry is built, otherwise a
|
||||
* no-update entry pinned to this version.
|
||||
* @param string $slug Plugin slug.
|
||||
* @param string $plugin_file Plugin basename.
|
||||
* @param array{name:string, type:string, new_version:string, security_patches:array<string,string>, requires_wp:string, requires_php:string, tested_up_to:string, page_url:string, description:string, icon_url:string, banner_url:string} $entry Normalised catalog entry.
|
||||
* @param string|null $current_version Installed version; when null an
|
||||
* update entry is built, otherwise a
|
||||
* no-update entry pinned to this version.
|
||||
* @param string $security_version Patch version when the update is
|
||||
* a security patch ('' otherwise).
|
||||
*
|
||||
* @return object stdClass for the transient bucket.
|
||||
*/
|
||||
private function build_update_object( string $slug, string $plugin_file, array $entry, ?string $current_version = null ): object {
|
||||
private function build_update_object( string $slug, string $plugin_file, array $entry, ?string $current_version = null, string $security_version = '' ): object {
|
||||
$is_no_update = null !== $current_version;
|
||||
|
||||
$data = array(
|
||||
'id' => $plugin_file,
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $is_no_update ? $current_version : $entry['new_version'],
|
||||
'new_version' => $is_no_update
|
||||
? $current_version
|
||||
: ( '' !== $security_version ? $security_version : $entry['new_version'] ),
|
||||
'url' => $entry['page_url'],
|
||||
'package' => $is_no_update ? '' : $this->package_url( $slug, $entry['type'] ),
|
||||
'package' => $is_no_update ? '' : $this->package_url( $slug, $entry['type'], $security_version ),
|
||||
'requires' => $entry['requires_wp'],
|
||||
'requires_php' => $entry['requires_php'],
|
||||
'tested' => $entry['tested_up_to'],
|
||||
|
|
@ -354,13 +424,15 @@ class Robotstxt_Manager_Updater {
|
|||
* Free plugins stream through the proxy without auth (Core 1.4.0+).
|
||||
* Premium plugins append the account API key as an api_key query parameter
|
||||
* (accepted by Core 1.5.0+) — the native upgrader cannot send headers.
|
||||
* Security patches add a validated `version` parameter (Core 1.16.0+).
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @param string $type Plugin type ('free' or 'premium').
|
||||
* @param string $security_version Patch version when serving a security patch.
|
||||
*
|
||||
* @return string Package URL.
|
||||
*/
|
||||
private function package_url( string $slug, string $type ): string {
|
||||
private function package_url( string $slug, string $type, string $security_version = '' ): string {
|
||||
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||
|
||||
$url = $client->get_store_url() . '/wp-json/robotstxt-core/v1/plugins/' . rawurlencode( $slug ) . '/download';
|
||||
|
|
@ -368,6 +440,10 @@ class Robotstxt_Manager_Updater {
|
|||
'domain' => $this->site_domain(),
|
||||
);
|
||||
|
||||
if ( '' !== $security_version ) {
|
||||
$args['version'] = rawurlencode( $security_version );
|
||||
}
|
||||
|
||||
if ( 'premium' === $type ) {
|
||||
// Preferred: a short-lived download token (Core 1.9.0+) — keeps the
|
||||
// long-lived API key out of the update transient and access logs.
|
||||
|
|
|
|||
Loading…
Reference in a new issue