oont-contents/plugins/wp-rocket/inc/Engine/Plugin/UpdaterSubscriber.php
2025-02-08 15:10:23 +01:00

511 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace WP_Rocket\Engine\Plugin;
use Plugin_Upgrader;
use Plugin_Upgrader_Skin;
use WP_Rocket\Event_Management\Event_Manager;
use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface;
/**
* Manages the plugin updates.
*/
class UpdaterSubscriber implements Event_Manager_Aware_Subscriber_Interface {
use UpdaterApiTools;
/**
* Full path to the plugin.
*
* @var string
*/
private $plugin_file;
/**
* Current version of the plugin.
*
* @var string
*/
private $plugin_version;
/**
* URL to the plugin provider.
*
* @var string
*/
private $vendor_url;
/**
* URL to contact to get update info.
*
* @var string
*/
private $api_url;
/**
* A list of plugins icon URLs.
*
* @var array {
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* @see https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
*/
private $icons;
/**
* An ID to use when a API request fails.
*
* @var string
*/
protected $request_error_id = 'rocket_update_failed';
/**
* Name of the transient that caches the update data.
*
* @var string
*/
protected $cache_transient_name = 'wp_rocket_update_data';
/**
* The WordPress Event Manager
*
* @var Event_Manager;
*/
protected $event_manager;
/**
* Constructor
*
* @param array $args {
* Required arguments to populate the class properties.
*
* @type string $plugin_file Full path to the plugin.
* @type string $plugin_version Current version of the plugin.
* @type string $vendor_url URL to the plugin provider.
* @type string $api_url URL to contact to get update info.
* }
*/
public function __construct( $args ) {
foreach ( [ 'plugin_file', 'plugin_version', 'vendor_url', 'api_url', 'icons' ] as $setting ) {
if ( isset( $args[ $setting ] ) ) {
$this->$setting = $args[ $setting ];
}
}
}
/**
* {@inheritdoc}
*
* @param Event_Manager $event_manager The WordPress Event Manager.
*/
public function set_event_manager( Event_Manager $event_manager ) {
$this->event_manager = $event_manager;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'http_request_args' => [ 'exclude_rocket_from_wp_updates', 5, 2 ],
'pre_set_site_transient_update_plugins' => 'maybe_add_rocket_update_data',
'deleted_site_transient' => 'maybe_delete_rocket_update_data_cache',
'wp_rocket_loaded' => 'maybe_force_check',
'auto_update_plugin' => [ 'disable_auto_updates', 10, 2 ],
'admin_post_rocket_rollback' => 'rollback',
];
}
/**
* When WP checks plugin versions against the latest versions hosted on WordPress.org, remove WPR from the list.
*
* @see wp_update_plugins()
*
* @param array $request An array of HTTP request arguments.
* @param string $url The request URL.
* @return array Updated array of HTTP request arguments.
*/
public function exclude_rocket_from_wp_updates( $request, $url ) {
if ( ! is_string( $url ) ) {
return $request;
}
if ( ! preg_match( '@^https?://api.wordpress.org/plugins/update-check(/|\?|$)@', $url ) || empty( $request['body']['plugins'] ) ) {
// Not a plugin update request. Stop immediately.
return $request;
}
/**
* Depending on the API version, the data can have several forms:
* - Can be serialized or JSON encoded,
* - Can be an object of arrays or an object of objects.
*/
$is_serialized = is_serialized( $request['body']['plugins'] );
$basename = plugin_basename( $this->plugin_file );
$edited = false;
if ( $is_serialized ) {
$plugins = maybe_unserialize( $request['body']['plugins'] );
} else {
$plugins = json_decode( $request['body']['plugins'] );
}
if ( ! empty( $plugins->plugins ) ) {
if ( is_object( $plugins->plugins ) ) {
if ( isset( $plugins->plugins->$basename ) ) {
unset( $plugins->plugins->$basename );
$edited = true;
}
} elseif ( is_array( $plugins->plugins ) ) {
if ( isset( $plugins->plugins[ $basename ] ) ) {
unset( $plugins->plugins[ $basename ] );
$edited = true;
}
}
}
if ( ! empty( $plugins->active ) ) {
$active_is_object = is_object( $plugins->active );
if ( $active_is_object || is_array( $plugins->active ) ) {
foreach ( $plugins->active as $key => $plugin_basename ) {
if ( $plugin_basename !== $basename ) {
continue;
}
if ( $active_is_object ) {
unset( $plugins->active->$key );
} else {
unset( $plugins->active[ $key ] );
}
$edited = true;
break;
}
}
}
if ( $edited ) {
if ( $is_serialized ) {
$request['body']['plugins'] = maybe_serialize( $plugins );
} else {
$request['body']['plugins'] = wp_json_encode( $plugins );
}
}
return $request;
}
/**
* Add WPR update data to the "WP update" transient.
*
* @param \stdClass $transient_value New value of site transient.
* @return \stdClass
*/
public function maybe_add_rocket_update_data( $transient_value ) {
if ( defined( 'WP_INSTALLING' ) ) {
return $transient_value;
}
// Get the remote version data.
$remote_data = $this->get_cached_latest_version_data();
if ( is_wp_error( $remote_data ) ) {
return $transient_value;
}
// Make sure the transient value is well formed.
if ( ! is_object( $transient_value ) ) {
$transient_value = new \stdClass();
}
if ( empty( $transient_value->response ) ) {
$transient_value->response = [];
}
if ( empty( $transient_value->checked ) ) {
$transient_value->checked = [];
}
// If a newer version is available, add the update.
if ( version_compare( $this->plugin_version, $remote_data->new_version, '<' ) ) {
$transient_value->response[ $remote_data->plugin ] = $remote_data;
}
$transient_value->checked[ $remote_data->plugin ] = $this->plugin_version;
return $transient_value;
}
/**
* Delete WPR update data cache when the "WP update" transient is deleted.
*
* @param string $transient_name Deleted transient name.
*/
public function maybe_delete_rocket_update_data_cache( $transient_name ) {
if ( 'update_plugins' === $transient_name ) {
$this->delete_rocket_update_data_cache();
}
}
/**
* If the `rocket_force_update` query arg is set, force WP to refresh the list of plugins to update.
*/
public function maybe_force_check() {
if ( is_string( filter_input( INPUT_GET, 'rocket_force_update' ) ) ) {
delete_site_transient( 'update_plugins' );
}
}
/**
* Disable auto-updates for WP Rocket
*
* @param bool|null $update Whether to update. The value of null is internally used to detect whether nothing has hooked into this filter.
* @param object $item The update offer.
* @return bool|null
*/
public function disable_auto_updates( $update, $item ) {
if ( isset( $item->plugin ) && ( 'wp-rocket/wp-rocket.php' === $item->plugin ) ) {
return false;
}
return $update;
}
/**
* Get the latest WPR update data from our server.
*
* @return \stdClass|\WP_Error {
* A \WP_Error object on failure. An object on success:
*
* @type string $slug The plugin slug.
* @type string $plugin The plugin base name.
* @type string $new_version The plugin new version.
* @type string $url URL to the plugin provider.
* @type string $package URL to the zip file of the new version.
* @type array $icons {
* A list of plugins icon URLs.
*
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* }
*/
public function get_latest_version_data() {
$request = wp_remote_get(
$this->api_url,
[
'timeout' => 30,
]
);
if ( is_wp_error( $request ) ) {
return $this->get_request_error(
[
'error_code' => $request->get_error_code(),
'response' => $request->get_error_message(),
]
);
}
$res = trim( wp_remote_retrieve_body( $request ) );
$code = wp_remote_retrieve_response_code( $request );
if ( 200 !== $code ) {
/**
* If the response doesnt have a status 200: it is an error, or there is no new update.
*/
return $this->get_request_error(
[
'http_code' => $code,
'response' => $res,
]
);
}
/**
* This will match:
* - `2.3.4.5-beta1||1.2.3.4-beta2||||||||||||||||||||||||||||||||`: expired license.
* - `2.3.4.5-beta1|https://wp-rocket.me/i-should-write-a-funny-thing-here/wp-rocket_1.2.3.4-beta2.zip|1.2.3.4-beta2`: valid license.
*/
if ( ! preg_match( '@^(?<stable_version>\d+(?:\.\d+){1,3}[^|]*)\|(?<package>(?:http.+\.zip)?)\|(?<user_version>\d+(?:\.\d+){1,3}[^|]*)(?:\|+)?$@', $res, $match ) ) {
/**
* If the response doesnt have the right format, it is an error.
*/
return $this->get_request_error( $res );
}
$obj = new \stdClass();
$obj->slug = $this->get_plugin_slug( $this->plugin_file );
$obj->plugin = plugin_basename( $this->plugin_file );
$obj->new_version = $match['user_version'];
$obj->url = $this->vendor_url;
$obj->package = $match['package'];
/**
* Filters the WP tested version value
*
* @since 3.10.7
*
* @param string $wp_tested_version WP tested version value.
*/
$obj->tested = apply_filters( 'rocket_wp_tested_version', WP_ROCKET_WP_VERSION_TESTED );
if ( $this->icons && ! empty( $this->icons['1x'] ) ) {
$obj->icons = $this->icons;
}
return $obj;
}
/**
* Get the cached version of the latest WPR update data.
*
* @return \stdClass|\WP_Error {
* A \WP_Error object on failure. An object on success:
*
* @type string $slug The plugin slug.
* @type string $plugin The plugin base name.
* @type string $new_version The plugin new version.
* @type string $url URL to the plugin provider.
* @type string $package URL to the zip file of the new version.
* @type array $icons {
* A list of plugins icon URLs.
*
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* }
*/
public function get_cached_latest_version_data() {
static $response;
if ( isset( $response ) ) {
// "force update" wont bypass the static cache: only one http request by page load.
return $response;
}
$force_update = is_string( filter_input( INPUT_GET, 'rocket_force_update' ) );
if ( ! $force_update ) {
// No "force update": try to get the result from a transient.
$response = get_site_transient( $this->cache_transient_name );
if ( $response && is_object( $response ) ) {
// Got something in cache.
return $response;
}
}
// Get fresh data.
$response = $this->get_latest_version_data();
$cache_duration = 12 * HOUR_IN_SECONDS;
if ( is_wp_error( $response ) ) {
$error_data = $response->get_error_data();
if ( ! empty( $error_data['error_code'] ) ) {
// `wp_remote_get()` returned an internal error ('error_code' contains a WP_Error code ).
$cache_duration = HOUR_IN_SECONDS;
} elseif ( ! empty( $error_data['http_code'] ) && $error_data['http_code'] >= 400 ) {
// We got a 4xx or 5xx HTTP error.
$cache_duration = 2 * HOUR_IN_SECONDS;
}
}
set_site_transient( $this->cache_transient_name, $response, $cache_duration );
return $response;
}
/**
* Delete WP Rocket update data cache.
*/
public function delete_rocket_update_data_cache() {
delete_site_transient( $this->cache_transient_name );
}
/**
* Do the rollback
*
* @since 2.4
*/
public function rollback() {
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rocket_rollback' ) ) {
wp_nonce_ays( '' );
}
if ( ! current_user_can( 'rocket_manage_options' ) ) {
wp_nonce_ays( '' );
}
/**
* Fires before doing the rollback
*/
do_action( 'rocket_before_rollback' );
$plugin_transient = get_site_transient( 'update_plugins' );
$plugin_folder = plugin_basename( dirname( WP_ROCKET_FILE ) );
$plugin = $plugin_folder . '/' . basename( WP_ROCKET_FILE );
$plugin_transient->response[ $plugin ] = (object) [
'slug' => $plugin_folder,
'new_version' => WP_ROCKET_LASTVERSION,
'url' => 'https://wp-rocket.me',
'package' => sprintf( 'https://wp-rocket.me/%s/wp-rocket_%s.zip', get_rocket_option( 'consumer_key' ), WP_ROCKET_LASTVERSION ),
];
$this->event_manager->remove_callback( 'pre_set_site_transient_update_plugins', [ $this, 'maybe_add_rocket_update_data' ] );
set_site_transient( 'update_plugins', $plugin_transient );
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// translators: %s is the plugin name.
$title = sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME );
$nonce = 'upgrade-plugin_' . $plugin;
$url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin );
$upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) );
$upgrader = new Plugin_Upgrader( $upgrader_skin );
add_filter( 'update_plugin_complete_actions', [ $this, 'rollback_add_return_link' ] );
rocket_put_content( WP_CONTENT_DIR . '/advanced-cache.php', '' );
$upgrader->upgrade( $plugin );
wp_die(
'',
// translators: %s is the plugin name.
esc_html( sprintf( __( '%s Update Rollback', 'rocket' ), WP_ROCKET_PLUGIN_NAME ) ),
[
'response' => 200,
]
);
}
/**
* After a rollback has been done, replace the "return to" link by a link pointing to WP Rocket's tools page.
* A link to the plugins page is kept in case the plugin is not reactivated correctly.
*
* @since 3.2.4
*
* @param array $update_actions Array of plugin action links.
* @return array The array of links where the "return to" link has been replaced.
*/
public function rollback_add_return_link( $update_actions ) {
if ( ! isset( $update_actions['plugins_page'] ) ) {
return $update_actions;
}
$update_actions['plugins_page'] = sprintf(
/* translators: 1 and 3 are link openings, 2 is a link closing. */
__( '%1$sReturn to WP Rocket%2$s or %3$sgo to Plugins page%2$s', 'rocket' ),
'<a href="' . esc_url( admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG ) . '#tools' ) . '" target="_parent">',
'</a>',
'<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '" target="_parent">'
);
return $update_actions;
}
}