v1.8.1
This commit is contained in:
parent
1460cb231f
commit
27ce69c2d2
9 changed files with 188 additions and 61 deletions
|
|
@ -322,7 +322,13 @@ class Robotstxt_Manager_Core_Client {
|
|||
$typed = array();
|
||||
foreach ( $cached as $slug => $row ) {
|
||||
if ( is_string( $slug ) && is_array( $row ) ) {
|
||||
$typed[ $slug ] = $row;
|
||||
$typed_row = array();
|
||||
foreach ( $row as $k => $v ) {
|
||||
if ( is_string( $k ) ) {
|
||||
$typed_row[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
$typed[ $slug ] = $typed_row;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,6 +405,12 @@ class Robotstxt_Manager_Core_Client {
|
|||
* Exchanges the account API key for a short-lived download token
|
||||
* (Core 1.9.0+ `POST /me/download-token`).
|
||||
*
|
||||
* Tokens are cached in a short-TTL site transient (5 minutes, a fraction
|
||||
* of the 15-minute token lifetime) because the updater rebuilds package
|
||||
* URLs on every read of the update_plugins transient. Failed exchanges
|
||||
* are negatively cached for one minute so a slow or down store is not
|
||||
* queried on every read either.
|
||||
*
|
||||
* @param string $slug Plugin slug the token may download.
|
||||
*
|
||||
* @return string Token string, or '' when unavailable (older Core, no
|
||||
|
|
@ -410,6 +422,13 @@ class Robotstxt_Manager_Core_Client {
|
|||
return '';
|
||||
}
|
||||
|
||||
$cache_key = 'robotstxt_manager_dl_token_' . sanitize_key( $slug );
|
||||
$cached = get_site_transient( $cache_key );
|
||||
|
||||
if ( is_string( $cached ) ) {
|
||||
return $cached; // Token, or '' from a negatively cached failure.
|
||||
}
|
||||
|
||||
// Send this site's domain so per-domain license binding is enforced
|
||||
// at token issuance (Core 1.11.0+); older Core ignores the field.
|
||||
$host = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
|
||||
|
|
@ -434,14 +453,25 @@ class Robotstxt_Manager_Core_Client {
|
|||
);
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
||||
set_site_transient( $cache_key, '', MINUTE_IN_SECONDS );
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
$token = is_array( $data ) ? ( $data['token'] ?? '' ) : '';
|
||||
$token = is_string( $token ) ? $token : '';
|
||||
|
||||
return is_string( $token ) ? $token : '';
|
||||
if ( '' === $token ) {
|
||||
set_site_transient( $cache_key, '', MINUTE_IN_SECONDS );
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
set_site_transient( $cache_key, $token, 5 * MINUTE_IN_SECONDS );
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@ class Robotstxt_Manager_Encryption {
|
|||
* @return string 32-byte raw key.
|
||||
*/
|
||||
private static function derive_key(): string {
|
||||
$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'auth_key_not_defined';
|
||||
$auth_salt = defined( 'AUTH_SALT' ) ? AUTH_SALT : 'auth_salt_not_defined';
|
||||
$auth_key = defined( 'AUTH_KEY' ) && is_string( AUTH_KEY ) ? AUTH_KEY : 'auth_key_not_defined';
|
||||
$auth_salt = defined( 'AUTH_SALT' ) && is_string( AUTH_SALT ) ? AUTH_SALT : 'auth_salt_not_defined';
|
||||
|
||||
return substr(
|
||||
hash_hmac( 'sha256', self::CONTEXT, $auth_key . $auth_salt, true ),
|
||||
|
|
@ -214,8 +214,8 @@ class Robotstxt_Manager_Encryption {
|
|||
* @return string 32-byte raw key.
|
||||
*/
|
||||
private static function derive_mac_key(): string {
|
||||
$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'auth_key_not_defined';
|
||||
$auth_salt = defined( 'AUTH_SALT' ) ? AUTH_SALT : 'auth_salt_not_defined';
|
||||
$auth_key = defined( 'AUTH_KEY' ) && is_string( AUTH_KEY ) ? AUTH_KEY : 'auth_key_not_defined';
|
||||
$auth_salt = defined( 'AUTH_SALT' ) && is_string( AUTH_SALT ) ? AUTH_SALT : 'auth_salt_not_defined';
|
||||
|
||||
return substr(
|
||||
hash_hmac( 'sha256', self::MAC_CONTEXT, $auth_key . $auth_salt, true ),
|
||||
|
|
|
|||
|
|
@ -16,11 +16,19 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* show the standard "Update available" badge and update through the regular
|
||||
* wp-admin flow, with the store's download proxy as the package source.
|
||||
*
|
||||
* - `pre_set_site_transient_update_plugins`: adds entries to ->response for
|
||||
* installed catalog plugins with a newer remote version, and to ->no_update
|
||||
* for up-to-date ones (prevents false WordPress.org matches).
|
||||
* - `site_transient_update_plugins`: injects entries into ->response for
|
||||
* installed catalog plugins with a newer remote version, and into ->no_update
|
||||
* for up-to-date ones (prevents false WordPress.org matches). The injection
|
||||
* runs on the READ side of the transient, so it works even when a full
|
||||
* wp_update_plugins() cycle never completes — for example on hosts where
|
||||
* api.wordpress.org is unreachable, where WordPress bails before building
|
||||
* the transient and write-side injection would never fire.
|
||||
* - `plugins_api`: serves the "View details" modal from catalog data.
|
||||
*
|
||||
* Local install state is resolved directly from get_plugins() (object-cached
|
||||
* per request) instead of the transient's ->checked list, which is only
|
||||
* populated by a completed WordPress.org check.
|
||||
*
|
||||
* Plugins that bundle their own update SDK already inject their own entries;
|
||||
* Manager never overwrites an existing response entry.
|
||||
*/
|
||||
|
|
@ -34,26 +42,23 @@ class Robotstxt_Manager_Updater {
|
|||
* @return void
|
||||
*/
|
||||
public function register( Robotstxt_Manager_Loader $loader ): void {
|
||||
$loader->add_filter( 'pre_set_site_transient_update_plugins', $this, 'inject_updates' );
|
||||
$loader->add_filter( 'site_transient_update_plugins', $this, 'inject_updates' );
|
||||
$loader->add_filter( 'plugins_api', $this, 'plugins_api_filter', 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects catalog update data into the WordPress update transient.
|
||||
*
|
||||
* @param mixed $transient The update_plugins transient object.
|
||||
* Read-side filter for get_site_transient( 'update_plugins' ): every
|
||||
* consumer (Plugins screen, Updates page, WP-CLI, auto-updates) passes
|
||||
* through here, so catalog updates surface regardless of whether a full
|
||||
* WordPress.org update-check cycle has completed.
|
||||
*
|
||||
* @return mixed Modified transient.
|
||||
* @param mixed $transient The stored update_plugins transient (object or false).
|
||||
*
|
||||
* @return mixed Transient object with catalog entries injected.
|
||||
*/
|
||||
public function inject_updates( mixed $transient ): mixed {
|
||||
if ( ! is_object( $transient )
|
||||
|| ! property_exists( $transient, 'checked' )
|
||||
|| ! is_array( $transient->checked )
|
||||
|| empty( $transient->checked )
|
||||
) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||
|
||||
if ( ! $client->is_configured() ) {
|
||||
|
|
@ -66,48 +71,112 @@ class Robotstxt_Manager_Updater {
|
|||
return $transient;
|
||||
}
|
||||
|
||||
$local = $this->local_plugin_versions();
|
||||
|
||||
if ( array() === $local ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$entries = $this->catalog_by_slug( $catalog );
|
||||
|
||||
foreach ( $transient->checked as $plugin_file => $raw_version ) {
|
||||
$version = is_string( $raw_version ) ? $raw_version : '';
|
||||
$slug = $this->slug_from_file( (string) $plugin_file );
|
||||
$entry = $entries[ $slug ] ?? null;
|
||||
$updates = ( $transient instanceof stdClass ) ? $transient : new stdClass();
|
||||
|
||||
if ( null === $entry || '' === $version ) {
|
||||
if ( ! property_exists( $updates, 'response' ) || ! is_array( $updates->response ) ) {
|
||||
$updates->response = array();
|
||||
}
|
||||
|
||||
if ( ! property_exists( $updates, 'no_update' ) || ! is_array( $updates->no_update ) ) {
|
||||
$updates->no_update = array();
|
||||
}
|
||||
|
||||
foreach ( $local as $plugin_file => $version ) {
|
||||
$slug = $this->slug_from_file( $plugin_file );
|
||||
$entry = $entries[ $slug ] ?? null;
|
||||
|
||||
if ( null === $entry || '' === $version || '' === $entry['new_version'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_version = $entry['new_version'];
|
||||
// Never overwrite an entry injected by the plugin's own SDK or
|
||||
// by WordPress.org. Entries Manager itself produced earlier are
|
||||
// the exception: WordPress persists the filtered read during
|
||||
// wp_update_plugins(), so on hosts where api.wordpress.org is
|
||||
// unreachable a stale Manager entry would otherwise occupy the
|
||||
// slot forever and mask newer catalog versions.
|
||||
$occupied = $updates->response[ $plugin_file ] ?? null;
|
||||
|
||||
if ( '' === $new_version ) {
|
||||
if ( null !== $occupied && ! $this->is_own_entry( $occupied, $client ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Never overwrite an entry injected by the plugin's own SDK.
|
||||
if ( property_exists( $transient, 'response' )
|
||||
&& is_array( $transient->response )
|
||||
&& isset( $transient->response[ $plugin_file ] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( version_compare( $version, $new_version, '<' ) ) {
|
||||
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
|
||||
// 403, so skip injecting the entry.
|
||||
// 403, so drop any stale own entry and skip.
|
||||
if ( 'premium' === $entry['type'] && ! $this->has_api_key() ) {
|
||||
unset( $updates->response[ $plugin_file ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( property_exists( $transient, 'response' ) && is_array( $transient->response ) ) {
|
||||
$transient->response[ $plugin_file ] = $this->build_update_object( $slug, (string) $plugin_file, $entry );
|
||||
$updates->response[ $plugin_file ] = $this->build_update_object( $slug, $plugin_file, $entry );
|
||||
unset( $updates->no_update[ $plugin_file ] );
|
||||
} else {
|
||||
// Up to date: a stale own response entry must go, or the
|
||||
// Plugins screen would keep offering a phantom update.
|
||||
unset( $updates->response[ $plugin_file ] );
|
||||
|
||||
if ( ! isset( $updates->no_update[ $plugin_file ] ) ) {
|
||||
$updates->no_update[ $plugin_file ] = $this->build_update_object( $slug, $plugin_file, $entry, $version );
|
||||
}
|
||||
} elseif ( property_exists( $transient, 'no_update' ) && is_array( $transient->no_update ) ) {
|
||||
$transient->no_update[ $plugin_file ] = $this->build_update_object( $slug, (string) $plugin_file, $entry, $version );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a response entry was produced by Manager itself (or by
|
||||
* a bundled SDK pulling from the same store), by comparing the package
|
||||
* URL host against the configured store host.
|
||||
*
|
||||
* @param mixed $entry Existing response entry.
|
||||
* @param Robotstxt_Manager_Core_Client $client Configured core client.
|
||||
*
|
||||
* @return bool True when the entry's package comes from this store.
|
||||
*/
|
||||
private function is_own_entry( mixed $entry, Robotstxt_Manager_Core_Client $client ): bool {
|
||||
if ( ! is_object( $entry ) || ! isset( $entry->package ) || ! is_string( $entry->package ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$store_host = strtolower( (string) wp_parse_url( $client->get_store_url(), PHP_URL_HOST ) );
|
||||
$entry_host = strtolower( (string) wp_parse_url( $entry->package, PHP_URL_HOST ) );
|
||||
|
||||
return '' !== $store_host && $store_host === $entry_host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installed plugin versions, from the object-cached plugin list.
|
||||
*
|
||||
* @return array<string, string> Map of plugin file ("slug/file.php") to version.
|
||||
*/
|
||||
private function local_plugin_versions(): array {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$versions = array();
|
||||
|
||||
foreach ( get_plugins() as $file => $data ) {
|
||||
if ( ! is_string( $file ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$version = isset( $data['Version'] ) && is_string( $data['Version'] ) ? $data['Version'] : '';
|
||||
$versions[ $file ] = $version;
|
||||
}
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,7 +205,7 @@ class Robotstxt_Manager_Updater {
|
|||
return $result;
|
||||
}
|
||||
|
||||
$slug = sanitize_key( (string) $args->slug );
|
||||
$slug = is_string( $args->slug ) ? sanitize_key( $args->slug ) : '';
|
||||
$entries = $this->catalog_by_slug( $catalog );
|
||||
$entry = $entries[ $slug ] ?? null;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue