JezK
Edit File: Updater.php
<?php namespace Appsero; use stdClass; /** * Appsero Updater * * This class will show new updates for the project */ class Updater { /** * Appsero\Client * * @var object */ protected $client; /** * Object of Updater * * @var object */ protected static $instance; /** * Cache key * * @var string */ protected $cache_key; /** * Initialize the class * * @param object $client */ public function __construct($client) { $this->client = $client; $this->cache_key = 'appsero_' . md5(sanitize_key($this->client->slug)) . '_version_info'; add_filter('upgrader_pre_download', [$this , 'validate_plugin_update_url'], 10, 2); // Run hooks based on the client type if ($this->client->type === 'plugin') { $this->run_plugin_hooks(); } elseif ($this->client->type === 'theme') { $this->run_theme_hooks(); } } /** * Initialize the Updater * * @param object $client * @return object */ public static function init($client) { if (!self::$instance) { self::$instance = new self($client); } return self::$instance; } /** * Set up WordPress filter hooks to get plugin updates * * @return void */ public function run_plugin_hooks() { // Register on every request, not only in wp-admin. WordPress rebuilds the // update_plugins transient from the wp_update_plugins cron event, which drives // auto-updates and never fires admin_init; registering these inside admin_init // left the plugin out of that transient and blanked its Automatic Updates column. add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_plugin_update' ] ); add_filter( 'plugins_api', [ $this, 'plugins_api_filter' ], 10, 3 ); add_action( 'admin_init', [ $this, 'admin_init' ] ); } /** * Set up WordPress filter hooks to get theme updates * * @return void */ public function run_theme_hooks() { add_filter('pre_set_site_transient_update_themes', [$this, 'check_theme_update']); } /** * Initialize the admin-only hooks * * @return void */ public function admin_init() { $this->show_warning_notice(); } /** * Check for plugin updates * * @param stdClass|bool $transient_data * @return stdClass */ public function check_plugin_update( $transient_data ) { global $pagenow; if ( ! is_object( $transient_data ) ) { $transient_data = new stdClass(); } if ( 'plugins.php' === $pagenow && is_multisite() ) { return $transient_data; } $version_info = $this->get_version_info(); if ( $version_info && is_object( $version_info ) && ! empty( $version_info->new_version ) ) { unset( $version_info->sections ); $key = version_compare( $this->client->project_version, $version_info->new_version, '<' ) ? 'response' : 'no_update'; if ( 'response' === $key && empty( $version_info->package ) ) { // AppSero omits the package when the license is not active. Without a package // WP_Automatic_Updater retries the update on every cron run and emails the admin // each time it fails, so opt the entry out of unattended updates. Core still // renders the update row and its "automatic update is unavailable" notice, which // is the correct prompt to renew the license. Only meaningful on a response entry: // WP_Automatic_Updater::should_update() never sees no_update. $version_info->disable_autoupdate = true; } $transient_data->{$key}[ $this->client->basename ] = $version_info; $transient_data->last_checked = time(); $transient_data->checked[ $this->client->basename ] = $this->client->project_version; } return $transient_data; } /** * Get cached version info from the database * * @return object|bool */ private function get_cached_version_info() { global $pagenow; // If updater page, fetch from API now if ('update-core.php' === $pagenow) { return false; // Force to fetch data } $value = get_transient($this->cache_key); if (!$value || !isset($value->name)) { return false; // Cache is expired } // Turn the icons into an array if (isset($value->icons)) { $value->icons = (array)$value->icons; } // Turn the banners into an array if (isset($value->banners)) { $value->banners = (array)$value->banners; } if (isset($value->sections)) { $value->sections = (array)$value->sections; } return $value; } /** * Set version info to the database * * @param object $value * @return void */ private function set_cached_version_info($value) { if (!$value) { return; } set_transient($this->cache_key, $value, 3 * HOUR_IN_SECONDS); } /** * Get project latest version info from Appsero * * @return object|bool */ private function get_project_latest_version() { $license = $this->client->license()->get_license(); $params = [ 'version' => $this->client->project_version, 'name' => sanitize_text_field($this->client->name), 'slug' => sanitize_key($this->client->slug), 'basename' => sanitize_text_field($this->client->basename), 'license_key' => !empty($license) && isset($license['key']) ? sanitize_text_field($license['key']) : '', ]; $route = 'v2/update/' . $this->client->hash . '/check'; $response = $this->client->send_request($params, $route, true); if (is_wp_error($response)) { return false; } $response = json_decode(wp_remote_retrieve_body($response)); if (!isset($response->slug)) { return false; } if (isset($response->icons)) { $response->icons = (array)$response->icons; } if (isset($response->banners)) { $response->banners = (array)$response->banners; } if (isset($response->sections)) { $response->sections = (array)$response->sections; } return $response; } /** * Update information on the "View version x.x details" page with custom data * * @param mixed $data * @param string $action * @param object $args * @return object */ public function plugins_api_filter($data, $action = '', $args = null) { if ($action !== 'plugin_information') { return $data; } if (!isset($args->slug) || ($args->slug !== $this->client->slug)) { return $data; } $version_info = $this->get_version_info(); if (false === $version_info || !is_object($version_info)) { return $data; } // WP core (install_plugin_install_status) reads $api->version; AppSero returns // new_version, so mirror it to avoid "Undefined property: version" and the // version_compare(null) deprecation on PHP 8. if (!isset($version_info->version) && isset($version_info->new_version)) { $version_info->version = $version_info->new_version; } return $version_info; } /** * Check for theme updates * * @param object $transient_data * @return object */ public function check_theme_update($transient_data) { global $pagenow; if (!is_object($transient_data)) { $transient_data = new stdClass(); } if ('themes.php' === $pagenow && is_multisite()) { return $transient_data; } if (!empty($transient_data->response) && !empty($transient_data->response[$this->client->slug])) { return $transient_data; } $version_info = $this->get_version_info(); if (false !== $version_info && is_object($version_info) && isset($version_info->new_version)) { // If new version available, set to response if (version_compare($this->client->project_version, $version_info->new_version, '<')) { $transient_data->response[$this->client->slug] = (array)$version_info; } else { // If new version is not available, set to no_update $transient_data->no_update[$this->client->slug] = (array)$version_info; } $transient_data->last_checked = time(); $transient_data->checked[$this->client->slug] = $this->client->project_version; } return $transient_data; } /** * Get version information * * @return object|bool */ private function get_version_info() { $version_info = $this->get_cached_version_info(); if (false === $version_info) { $version_info = $this->get_project_latest_version(); $this->set_cached_version_info($version_info); } return $version_info; } /** * Check required plugins * * @param array $required_plugins * @return array */ private function check_required_plugins($required_plugins = []) { $warnings = []; if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $installed_plugins = get_plugins(); foreach ($required_plugins as $plugin_slug => $required_version) { $plugin_file = $this->get_plugin_file($plugin_slug, $installed_plugins); $plugin_name = isset($installed_plugins[$plugin_file]) ? $installed_plugins[$plugin_file]['Name'] : $plugin_slug; if (!$plugin_file) { $warnings[] = $this->client->__trans( sprintf('%s (version %s) is required but not installed.', $plugin_name, $required_version) ); } elseif (!is_plugin_active($plugin_file)) { $warnings[] = $this->client->__trans( sprintf('%s (version %s) is required but not active.', $plugin_name, $required_version) ); } else { $installed_version = $installed_plugins[$plugin_file]['Version']; if (version_compare($installed_version, $required_version, '<')) { $warnings[] = $this->client->__trans( sprintf('%s requires version %s, but %s is installed. Please update %s.', $plugin_name, $required_version, $installed_version, $plugin_name) ); } } } return $warnings; } /** * Get plugin file from slug * * @param string $plugin_slug * @param array $installed_plugins * @return string|null */ private function get_plugin_file($plugin_slug, $installed_plugins) { foreach ($installed_plugins as $plugin_file => $plugin_info) { if (strpos($plugin_file, $plugin_slug . '/') === 0) { return $plugin_file; } } return null; } /** * Show warning notice for required plugins * * @return void */ protected function show_warning_notice() { $version_info = $this->get_version_info(); if ( false === $version_info || ! is_object( $version_info ) || ! isset( $version_info->new_version ) ) { return; } unset( $version_info->sections ); // If no new version is available, return early if ( version_compare( $this->client->project_version, $version_info->new_version, '>=' ) ) { return; } $required_plugins = isset( $version_info->required_plugins ) && ( is_array( $version_info->required_plugins ) || is_object( $version_info->required_plugins ) ) ? $version_info->required_plugins : []; $warnings = $this->check_required_plugins( $required_plugins ); if ( empty( $warnings ) ) { return; } add_filter( 'site_transient_update_plugins', function ( $transient ) { if ( ! is_object( $transient ) || empty( $transient->response ) || empty( $transient->response[ $this->client->basename ] ) ) { return $transient; } unset( $transient->response[ $this->client->basename ] ); return $transient; } ); add_action( "after_plugin_row_{$this->client->basename}", function ( $plugin_file, $plugin_data, $status ) use ( $warnings ) { $this->add_custom_plugin_row( $plugin_file, $plugin_data, $status, $warnings ); }, 10, 3 ); } /** * Add custom plugin row with warnings * * @param string $plugin_file * @param array $plugin_data * @param string $status * @param array $warnings * @return void */ public function add_custom_plugin_row($plugin_file, $plugin_data, $status, $warnings) { $plugin_slug = dirname($plugin_file); $wp_list_table = _get_list_table('WP_Plugins_List_Table', ['screen' => get_current_screen()]); $column_count = esc_attr($wp_list_table->get_column_count()); $version_info = $this->get_version_info(); $new_version = $version_info->new_version; printf( '<tr class="plugin-update-tr active" id="%s-update" data-slug="%s" data-plugin="%s">' . '<td colspan="%s" class="plugin-update colspanchange">' . '<div class="update-message notice inline notice-warning notice-alt">' . '<p>%s</p>' . '<p>%s</p>' . '</div></td></tr>', esc_attr($plugin_slug), esc_attr($plugin_slug), esc_attr($plugin_file), $column_count, esc_html(sprintf( __('%s %s is available. %s', 'appsero'), $plugin_data['Name'], $new_version, __('Please resolve the following issues to update the plugin:', 'appsero') )), esc_html(implode(' ', $warnings)) ); } public function validate_plugin_update_url($reply, $package) { if ( strpos( $package, 'https://api.appsero.com' ) !== 0 ) { return $reply; // Must be local files or other plugins that are not updated via Appsero. } $response = wp_remote_get($package, ['method' => 'HEAD', 'redirection' => 5]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) != 200) { $error_message = is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_body($response); if (empty($error_message)) { $error_message = wp_remote_retrieve_response_message($response); } return new \WP_Error('invalid_update_url', $error_message); } return $reply; } }