$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; } }