Files
2026-04-28 15:13:50 +02:00

333 lines
9.4 KiB
PHP

<?php
namespace AIOSEO\Plugin\Pro\Admin;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The updates class to check for updates from our server.
*
* @since 4.0.0
*/
class Updates {
use \AIOSEO\Plugin\Pro\Traits\Updates;
/**
* Plugin slug.
*
* @since 4.0.0
*
* @var bool|string
*/
public $pluginSlug = false;
/**
* Plugin path.
*
* @since 4.0.0
*
* @var bool|string
*/
public $pluginPath = false;
/**
* Version number of the plugin.
*
* @since 4.0.0
*
* @var bool|int
*/
public $version = false;
/**
* License key for the plugin.
*
* @since 4.0.0
*
* @var bool|string
*/
public $key = false;
/**
* Store the update data returned from the API.
*
* @since 4.0.0
*
* @var object
*/
public $update;
/**
* Store the plugin info details for the update.
*
* @since 4.0.0
*
* @var bool|object
*/
public $info = false;
/**
* Source of notifications content.
*
* @since 4.0.0
*
* @var string
*/
public $baseUrl = 'https://licensing.aioseo.com/v1/';
/**
* Primary class constructor.
*
* @since 4.0.0
*
* @param array $config Array of updater config args.
*/
public function __construct( array $config ) {
// Set class properties.
$acceptedArgs = [
'pluginSlug',
'pluginPath',
'version',
'key',
];
foreach ( $acceptedArgs as $arg ) {
$this->$arg = $config[ $arg ];
}
// Initialize the class on "init". This cannot run on "plugins_loaded"
// because that's when this class is constructed in the first place.
add_action( 'init', [ $this, 'init' ] );
}
/**
* Initialize the class.
*
* @since 4.7.0
*/
public function init() {
// If the current user cannot update plugins, stop processing here.
// We want to make sure a user is logged in because we need WP CLI and cron jobs to get past this check.
if ( is_user_logged_in() && ! current_user_can( 'update_plugins' ) ) {
return;
}
// Load the updater hooks and filters.
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'updatePluginsFilter' ], 1000 );
add_filter( 'plugins_api', [ $this, 'pluginsApi' ], 10, 3 );
if ( ! wp_doing_cron() ) {
add_filter( 'upgrader_package_options', [ $this, 'validateDownloadUrl' ] );
}
}
/**
* Add our Pro plugin update details when WordPress runs its update checker.
* Right before WordPress saves the plugin update object, we infuse it with our own data.
*
* @since 4.0.0
*
* @param mixed $value The WordPress update object.
* @return object Amended WordPress update object on success, default if object is empty.
*/
public function updatePluginsFilter( $value ) {
// If no update object exists, bail to prevent errors.
if ( empty( $value ) || ! is_object( $value ) ) {
return $value;
}
// If we haven't checked for update details, do so now.
// wp_update_plugins() sets the transient twice so we store the update to prevent a second redundant request.
// If the request fails, we will return a default object based on the current version of the plugin.
if ( empty( $this->update ) ) {
$this->update = $this->checkForUpdates();
if ( ! is_object( $this->update ) || ! empty( $this->update->error ) ) {
$this->update = new \stdClass();
return $value;
}
$this->update->description = $this->update->description ? preg_replace( '/\s+/', ' ', (string) $this->update->description ) : null;
$this->update->changelog = $this->update->changelog ? preg_replace( '/\s+/', ' ', (string) $this->update->changelog ) : null;
}
$this->update->icons = ! empty( $this->update->icons ) ? (array) $this->update->icons : [];
$this->update->aioseo = true;
$this->update->plugin = $this->pluginPath;
$this->update->oldVersion = $this->version;
// Infuse the update object with our data if the version from the remote API is newer.
if ( isset( $this->update->new_version ) && version_compare( $this->version, $this->update->new_version, '<' ) ) {
// The $plugin_update object contains new_version, package, slug, and last_update keys.
$value->response[ $this->pluginPath ] = $this->update;
} else {
$this->update->new_version = $this->version;
$this->update->plugin = $this->pluginPath;
$value->no_update[ $this->pluginPath ] = $this->update;
}
// Return the update object.
return $value;
}
/**
* Check for updates request.
*
* @since 4.0.0
*
* @return mixed The update object, or null if the request fails.
*/
public function checkForUpdates() {
$cacheKeyHash = sha1( $this->pluginSlug . $this->version . $this->key );
$cacheKey = "aioseo_update_check_{$cacheKeyHash}";
$cachedUpdate = aioseo()->core->networkCache->get( $cacheKey );
if ( null !== $cachedUpdate ) {
return $cachedUpdate;
}
$args = [
'license' => $this->key,
'domain' => aioseo()->helpers->getSiteDomain( true ),
'sku' => $this->pluginSlug,
'version' => $this->version,
'php_version' => PHP_VERSION,
'wp_version' => get_bloginfo( 'version' )
];
$response = aioseo()->helpers->wpRemotePost( $this->getUrl() . 'update/', [
'timeout' => 30,
'body' => wp_json_encode( $args )
] );
if ( is_wp_error( $response ) ) {
return null;
}
$update = json_decode( wp_remote_retrieve_body( $response ) );
// Validate the response has required properties
$isValid = ! empty( $update ) &&
is_object( $update ) &&
! property_exists( $update, 'error' ) &&
property_exists( $update, 'new_version' );
// Cache for 1 hour if valid, 10 minutes if invalid.
$cacheDuration = $isValid ? HOUR_IN_SECONDS : 10 * MINUTE_IN_SECONDS;
aioseo()->core->networkCache->update( $cacheKey, $update, $cacheDuration );
return $update;
}
/**
* Filter the plugins_api function to get our own custom plugin information
* from our private repo.
*
* @since 4.0.0
*
* @param object $api The original plugins_api object.
* @param string $action The action sent by plugins_api.
* @param object $args Additional args to send to plugins_api.
* @return object New stdClass with plugin information on success, default response on failure.
*/
public function pluginsApi( $api, $action = '', $args = null ) {
$plugin = ( 'plugin_information' === $action ) && isset( $args->slug ) && ( $this->pluginSlug === $args->slug );
// If our plugin matches the request, set our own plugin data, else return the default response.
if ( $plugin ) {
return $this->setPluginsApi( $api );
}
return $api;
}
/**
* Ping a remote API to retrieve plugin information for WordPress to display.
*
* @since 4.0.0
*
* @param object $defaultApi The default API object.
* @return object Return custom plugin information to plugins_api.
*/
public function setPluginsApi( $defaultApi ) {
// Perform the remote request to retrieve our plugin information. If it fails, return the default object.
if ( ! $this->info ) {
$cacheKey = 'plugin_info_' . sha1( $this->pluginSlug . $this->version . $this->key );
$cachedInfo = aioseo()->core->networkCache->get( $cacheKey );
if ( null !== $cachedInfo ) {
if ( false === $cachedInfo ) {
return $defaultApi;
}
$this->info = $cachedInfo;
} else {
$rawResponse = aioseo()->helpers->wpRemotePost( $this->getUrl() . 'info/', [
'timeout' => 20,
'body' => wp_json_encode( [
'license' => $this->key,
'domain' => aioseo()->helpers->getSiteDomain( true ),
'sku' => $this->pluginSlug,
'version' => $this->version,
'php_version' => PHP_VERSION,
'wp_version' => get_bloginfo( 'version' )
] )
] );
if ( is_wp_error( $rawResponse ) ) {
return $defaultApi;
}
$response = json_decode( wp_remote_retrieve_body( $rawResponse ) );
if ( empty( $response ) || property_exists( $response, 'error' ) ) {
$this->info = false;
aioseo()->core->networkCache->update( $cacheKey, false, 10 * MINUTE_IN_SECONDS );
return $defaultApi;
}
$this->info = $response;
aioseo()->core->networkCache->update( $cacheKey, $response, HOUR_IN_SECONDS );
}
}
// Create a new stdClass object and populate it with our plugin information.
$api = new \stdClass();
$api->name = $this->info->name ?? '';
$api->slug = $this->info->slug ?? '';
$api->version = $this->info->version ?? '';
$api->author = $this->info->author ?? '';
$api->author_profile = $this->info->author_profile ?? '';
$api->requires = $this->info->requires ?? '';
$api->tested = $this->info->tested ?? '';
$api->last_updated = $this->info->last_updated ?? '';
$api->homepage = $this->info->homepage ?? '';
$api->sections['description'] = $this->info->description ?? '';
$api->sections['changelog'] = $this->info->changelog ?? '';
$api->download_link = $this->info->download_link ?? '';
$api->active_installs = $this->info->active_installs ?? '';
$api->banners = isset( $this->info->banners ) ? (array) $this->info->banners : '';
// Return the new API object with our custom data.
return $api;
}
/**
* Get the URL to check licenses.
*
* @since 4.0.0
*
* @return string The URL.
*/
public function getUrl() {
if ( defined( 'AIOSEO_LICENSING_URL' ) ) {
return AIOSEO_LICENSING_URL;
}
return $this->baseUrl;
}
}