licences = $licences; if ( ( is_admin() ) || is_network_admin() || ( defined( 'WP_CLI' ) && class_exists( 'WP_CLI' ) ) ) { add_filter( 'plugins_api', array( $this, 'short_circuit_wordpress_org_plugin_info_request' ), 10, 3 ); add_filter( 'http_response', array( $this, 'verify_download' ), 10, 3 ); add_filter( 'plugins_api', array( $this, 'inject_addon_install_resource' ), 10, 3 ); add_action( 'current_screen', array( $this, 'check_again_clear_transients' ) ); } $this->add_plugin_notice( $this->licences->plugin->basename ); if ( $this->licences->addons ) { foreach ( $this->licences->addons as $basename => $addon ) { if ( ! ( $addon['available'] && $addon['installed'] ) ) { // Only register addons for updates that are installed and available for license continue; } $this->add_plugin_notice( $basename ); } } } /** * Add handlers for displaying update notices for a plugin on our settings page. * * @param string $basename */ public function add_plugin_notice( string $basename ) { if ( empty( $basename ) ) { return; } $this->plugin_notices[ $basename ] = $basename; } /** * Return update notice info for a plugin on our settings page. * * @return array */ public function get_plugin_update_notices(): array { $notices = array(); if ( ! empty( $this->plugin_notices ) ) { foreach ( $this->plugin_notices as $basename ) { $notice = $this->version_update_notice( $basename ); if ( ! empty( $notice ) ) { $notices[] = $notice; } } } return $notices; } /** * Adds the plugin to the array of plugins used by the update JS * * @param array $plugins * * @return array */ public function register_plugin_for_updates( $plugins ) { $plugins[ $this->licences->plugin->slug ] = array_merge( (array) $this->licences->plugin, array( 'addons' => $this->licences->addons, 'license' => $this->licences->is_licence_expired(), 'update_available' => $this->update_available( $this->licences->plugin->slug ), ) ); return $plugins; } /** * Take over the update check for plugins * * @param object $trans * * @return object */ function site_transient_update_plugins( $trans ) { if ( ! is_object( $trans ) ) { return $trans; } if ( ! isset( $trans->response ) || ! is_array( $trans->response ) ) { $trans->response = array(); } if ( ! isset( $trans->no_update ) || ! is_array( $trans->no_update ) ) { $trans->no_update = array(); } $plugin_upgrade_data = $this->get_upgrade_data(); $plugin_basename = $this->licences->plugin->get_plugin_basename( $this->licences->plugin->slug ); if ( isset( $trans->no_update[ $plugin_basename ] ) ) { // Ensure the pro plugin always has the correct info and WP API doesn't confuse with free version $trans->no_update[ $plugin_basename ]->slug = $this->licences->plugin->slug; $trans->no_update[ $plugin_basename ]->url = $this->licences->api_base; $trans->no_update[ $plugin_basename ]->new_version = $this->licences->plugin->version; } if ( false === $plugin_upgrade_data || ! isset( $plugin_upgrade_data[ $this->licences->plugin->slug ] ) ) { return $trans; } foreach ( $plugin_upgrade_data as $plugin_slug => $upgrade_data ) { $plugin_basename = $this->licences->plugin->get_plugin_basename( $plugin_slug ); if ( isset( $this->licences->addons[ $plugin_basename ] ) && ! ( $this->licences->addons[ $plugin_basename ]['available'] && $this->licences->addons[ $plugin_basename ]['installed'] ) ) { // Addon not installed or available for license, ignore continue; } $installed_version = $this->get_installed_version( $plugin_slug ); $latest_version = $this->get_latest_version( $plugin_slug, $installed_version ); if ( false === $installed_version || false === $latest_version ) { continue; } if ( version_compare( $installed_version, $latest_version, '<' ) ) { $is_beta = $this->is_beta_version( $latest_version ); $trans->response[ $plugin_basename ] = new stdClass(); $trans->response[ $plugin_basename ]->url = $this->licences->api_base; $trans->response[ $plugin_basename ]->slug = $plugin_slug; $trans->response[ $plugin_basename ]->package = $this->get_plugin_update_download_url( $plugin_slug, $is_beta ); $trans->response[ $plugin_basename ]->new_version = $latest_version; $trans->response[ $plugin_basename ]->id = '0'; $trans->response[ $plugin_basename ]->plugin = $plugin_basename; if ( isset( $upgrade_data['requires_php'] ) ) { $trans->response[ $plugin_basename ]->requires_php = $upgrade_data['requires_php']; } if ( isset( $upgrade_data['tested'] ) ) { $trans->response[ $plugin_basename ]->tested = $upgrade_data['tested']; } if ( isset( $upgrade_data['icon_url'] ) ) { $trans->response[ $plugin_basename ]->icons['svg'] = $upgrade_data['icon_url']; } } } return $trans; } /** * Add some custom JS into the plugin page for our updates process */ public function enqueue_plugin_update_script() { $handle = 'as3cf-dbrains-plugin-update-script'; // This script should only be enqueued once if the site has multiple // Delicious Brains plugins installed if ( wp_script_is( $handle, 'enqueued' ) ) { return; } $version = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? time() : $this->licences->plugin->version; $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; $src = plugins_url( "assets/js/plugin-update$min.js", __FILE__ ); wp_enqueue_script( $handle, $src, array( 'jquery', 'underscore' ), $version, true ); wp_localize_script( $handle, 'as3cf_dbrains', array( 'nonces' => array( 'check_licence' => wp_create_nonce( 'check-licence' ), ), 'strings' => array( 'check_licence_again' => __( 'Check My License Again' ), 'licence_check_problem' => __( 'A problem occurred when trying to check the license, please try again.' ), 'requires_parent_licence' => __( 'Requires a valid license for %s.' ), 'update_available' => __( 'New version available for active subscriptions.' ), ), 'plugins' => apply_filters( 'as3cf_plugins', array() ), ) ); $src = plugins_url( "assets/css/plugin-update.css", __FILE__ ); wp_enqueue_style( $handle, $src, array(), $version ); } /** * Short circuits the HTTP request to WordPress.org servers to retrieve plugin information. * Will only fire on the update-core.php admin page. * * @param object|bool $res Plugin resource object or boolean false. * @param string $action The API call being performed. * @param object $args Arguments for the API call being performed. * * @return object|bool Plugin resource object or boolean false. */ function short_circuit_wordpress_org_plugin_info_request( $res, $action, $args ) { if ( 'plugin_information' != $action || empty( $args->slug ) || $this->licences->plugin->slug != $args->slug ) { return $res; } $screen = get_current_screen(); // Only fire on the update-core.php admin page if ( empty( $screen->id ) || ( 'update-core' !== $screen->id && 'update-core-network' !== $screen->id ) ) { return $res; } $res = new stdClass(); $plugin_info = $this->get_upgrade_data(); if ( isset( $plugin_info[ $this->licences->plugin->slug ]['tested'] ) ) { $res->tested = $plugin_info[ $this->licences->plugin->slug ]['tested']; } else { $res->tested = false; } return $res; } /** * Clear update transients when the user clicks the "Check Again" button from the update screen. * * @param object $current_screen */ function check_again_clear_transients( $current_screen ) { if ( ! isset( $current_screen->id ) || false === strpos( $current_screen->id, 'update-core' ) || ! isset( $_GET['force-check'] ) ) { return; } delete_site_transient( $this->licences->plugin->prefix . '_upgrade_data' ); delete_site_transient( 'update_plugins' ); delete_site_transient( $this->licences->plugin->prefix . '_licence_response' ); delete_site_transient( 'dbrains_api_down' ); } /** * Display our custom plugin details when the user clicks "view details" * on the plugin listing page. */ function plugin_update_popup() { $slug = sanitize_key( $_GET['plugin'] ); if ( ! $this->is_plugin( $slug ) ) { return; } $error_msg = '
' . __( 'Could not retrieve version details. Please try again.' ) . '
'; $latest_version = $this->get_latest_version( $slug ); if ( false === $latest_version ) { echo $error_msg; exit; } $data = $this->licences->get_changelog( $slug, $this->is_beta_version( $latest_version ) ); if ( is_wp_error( $data ) || empty( $data ) ) { echo $error_msg; } else { echo $data; } exit; } /** * Verify the download URL for the plugin * * @param array $response * @param array $args * @param string $url * * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error * instance upon error */ function verify_download( $response, $args, $url ) { if ( is_wp_error( $response ) ) { return $response; } $download_url = $this->get_plugin_update_download_url( $this->licences->plugin->slug ); if ( false === strpos( $url, $download_url ) || 402 != $response['response']['code'] ) { return $response; } // The $response['body'] is blank but output is actually saved to a file in this case $data = @file_get_contents( $response['filename'] ); //phpcs:ignore if ( ! $data ) { return new WP_Error( $this->licences->plugin->prefix . '_download_error_empty', sprintf( __( 'Error retrieving download from deliciousbrain.com. Please try again or download manually from %2$s.' ), $this->licences->plugin->account_url, _x( 'My Account', 'Delicious Brains account' ) ) ); } $decoded_data = json_decode( $data, true ); // Can't decode the JSON errors, so just barf it all out if ( ! isset( $decoded_data['errors'] ) || ! $decoded_data['errors'] ) { return new WP_Error( $this->licences->plugin->prefix . '_download_error_raw', $data ); } foreach ( $decoded_data['errors'] as $key => $msg ) { return new WP_Error( $this->licences->plugin->prefix . '_' . $key, $msg ); } } /** * Is the plugin version a beta version * * @param string $ver * * @return bool */ function is_beta_version( $ver ) { if ( preg_match( '@b[0-9]+$@', $ver ) ) { return true; } return false; } /** * Get the installed version of a plugin * * @param string $slug * * @return int|string */ function get_installed_version( $slug ) { $meta_key = $this->licences->plugin->get_global_meta_key(); if ( isset( $GLOBALS[ $meta_key ][ $slug ]['version'] ) ) { // Plugin activated, use meta for version $installed_version = $GLOBALS[ $meta_key ][ $slug ]['version']; } else { // Not activated $installed_version = $this->licences->plugin->get_plugin_version( $slug ); } return $installed_version; } /** * Get the latest version available of a plugin * * @param string $slug * @param string|null $installed_version * * @return string|false */ function get_latest_version( $slug, $installed_version = null ) { $data = $this->get_upgrade_data(); if ( ! isset( $data[ $slug ] ) ) { return false; } $plugin_file = $this->licences->plugin->get_plugin_basename( $slug ); if ( $this->is_addon( $plugin_file ) ) { if ( ! $this->is_allowed_addon( $slug ) ) { return false; } } if ( is_null( $installed_version ) ) { $installed_version = $this->get_installed_version( $slug ); } $required_version = $this->get_required_version( $slug ); // Return the latest beta version if the installed version is beta // and the API returned a beta version and it's newer than the latest stable version if ( $installed_version && ( $this->is_beta_version( $installed_version ) || $this->is_beta_version( $required_version ) ) && isset( $data[ $slug ]['beta_version'] ) && version_compare( $data[ $slug ]['version'], $data[ $slug ]['beta_version'], '<' ) ) { return $data[ $slug ]['beta_version']; } return $data[ $slug ]['version']; } /** * Get the upgrade data about the plugin and its addons * * @return array|bool|mixed */ function get_upgrade_data() { $info = get_site_transient( $this->licences->plugin->prefix . '_upgrade_data' ); // Remove old format upgrade data. if ( isset( $info['version'] ) ) { delete_site_transient( $this->licences->plugin->prefix . '_licence_response' ); delete_site_transient( $this->licences->plugin->prefix . '_upgrade_data' ); $info = false; } if ( $info ) { return $info; } $data = $this->licences->get_upgrade_data(); $data = json_decode( $data, true ); /* We need to set the transient even when there's an error, otherwise we'll end up making API requests over and over again and slowing things down big time. */ $default_upgrade_data = array( $this->licences->plugin->slug => array( 'version' => $this->licences->plugin->version ) ); if ( ! $data ) { set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $default_upgrade_data, $this->licences->transient_retry_timeout ); $this->licences->log_error( 'Error trying to decode JSON upgrade data.' ); return false; } if ( isset( $data['errors'] ) ) { set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $default_upgrade_data, $this->licences->transient_retry_timeout ); $this->licences->log_error( __( 'Error trying to get upgrade data.' ), $data['errors'] ); return false; } set_site_transient( $this->licences->plugin->prefix . '_upgrade_data', $data, $this->licences->transient_timeout ); return $data; } /** * Create the plugin update URL * * @param string $plugin_slug * @param bool $is_beta * * @return string */ function get_plugin_update_download_url( $plugin_slug, $is_beta = false ) { $licence = $this->licences->get_licence_key(); $query_args = array( 'request' => 'download', 'licence_key' => $licence, 'slug' => $plugin_slug, 'product' => $this->licences->plugin->slug, 'site_url' => $this->licences->home_url, ); if ( $is_beta ) { $query_args['beta'] = '1'; } $url = add_query_arg( $query_args, $this->licences->api_url ); return esc_url_raw( $url ); } /** * Display custom version update notices to the top of our plugin page * * @param string $basename * * @return array */ private function version_update_notice( string $basename ): array { // We don't want to show both the "Update Required" and "Update Available" messages at the same time if ( $this->is_addon( $basename ) && $this->is_addon_outdated( $basename ) ) { return array(); } $slug = current( explode( '/', $basename ) ); // To reduce UI clutter we hide addon update notices if the core plugin has updates available if ( $this->is_addon( $basename ) && $this->core_update_available() ) { // Core update is available, don't show update notices for addons until core is updated return array(); } $installed_version = $this->get_installed_version( $slug ); $latest_version = $this->get_latest_version( $slug, $installed_version ); if ( version_compare( $installed_version, $latest_version, '<' ) ) { $plugin_name = ( isset( $this->licences->addons[ $basename ] ) ) ? $this->licences->addons[ $basename ]['name'] : $this->licences->plugin->name; $heading = _x( 'Update Available', 'A new version of the plugin is available' ); $message = sprintf( __( '%1$s %2$s is now available. You currently have %3$s installed.' ), $plugin_name, $latest_version, $installed_version ); $licence = $this->licences->get_licence_key(); $licence_response = $this->licences->is_licence_expired(); $licence_problem = isset( $licence_response['errors'] ); if ( ! empty( $licence ) && ! $licence_problem ) { $update_url = wp_nonce_url( $this->licences->admin_url( 'update.php?action=upgrade-plugin&plugin=' . urlencode( $basename ) ), 'upgrade-plugin_' . $basename ); $message .= ' ' . sprintf( '%2$s', $update_url, _x( 'Update Now', 'Download and install a new version of the plugin' ) ); } return array( 'slug' => $slug, 'heading' => $heading, 'message' => $message, ); } return array(); } /** * Is the plugin an addon * * @param string $plugin_file * * @return bool */ function is_addon( $plugin_file ) { return isset( $this->licences->addons[ $plugin_file ] ); } /** * Is there an update available for the given plugin slug? * * @param string $slug * * @return bool */ private function update_available( string $slug ): bool { $installed_version = $this->get_installed_version( $slug ); $latest_version = $this->get_latest_version( $slug, $installed_version ); return version_compare( $installed_version, $latest_version, '<' ); } /** * Is there an update for the core plugin? * * @return bool */ private function core_update_available(): bool { return $this->update_available( $this->licences->plugin->slug ); } /** * Check if an addon needs to be updated * * @param string $addon_basename * * @return bool */ function is_addon_outdated( $addon_basename ) { $addon_slug = current( explode( '/', $addon_basename ) ); $installed_version = $this->get_installed_version( $addon_slug ); $required_version = $this->licences->addons[ $addon_basename ]['required_version']; return version_compare( $installed_version, $required_version, '<' ); } /** * Is the addon allowed for the license * * @param string $slug * * @return bool */ function is_allowed_addon( $slug ) { $addons = get_site_transient( $this->licences->plugin->prefix . '_addons' ); if ( isset( $addons[ $slug ] ) ) { // Addon allowed return true; } // Not an allowed addon return false; } /** * Hook into the plugin install process and inject addon download url * * @param stdClass $res * @param string $action * @param stdClass $args * * @return stdClass */ function inject_addon_install_resource( $res, $action, $args ) { if ( 'plugin_information' != $action || empty( $args->slug ) ) { return $res; } $addons = get_site_transient( $this->licences->plugin->prefix . '_addons' ); if ( ! isset( $addons[ $args->slug ] ) ) { return $res; } $addon = $addons[ $args->slug ]; $required_version = $this->get_required_version( $args->slug ); $is_beta = $this->is_beta_version( $required_version ) && ! empty( $addon['beta_version'] ); $res = new stdClass(); $res->name = $this->licences->plugin->name . ' ' . $addon['name']; $res->version = $is_beta ? $addon['beta_version'] : $addon['version']; $res->download_link = $this->get_plugin_update_download_url( $args->slug, $is_beta ); $res->tested = isset( $addon['tested'] ) ? $addon['tested'] : false; return $res; } /** * Get the required version of a plugin * * @param string $slug * * @return string */ function get_required_version( $slug ) { $plugin_file = $this->licences->plugin->get_plugin_basename( $slug ); if ( isset( $this->licences->addons[ $plugin_file ]['required_version'] ) ) { return $this->licences->addons[ $plugin_file ]['required_version']; } else { return '0'; } } /** * Check a plugin slug to see if it is the core plugin or one of its addons * * @param string $slug * * @return bool */ function is_plugin( $slug ) { $plugins = array( $this->licences->plugin->slug ); if ( $this->licences->addons ) { foreach ( $this->licences->addons as $key => $addon ) { $plugins[] = dirname( $key ); } } return in_array( $slug, $plugins ); } /** * Helper to get the directory of the plugin * * @return string */ function plugins_dir() { $path = untrailingslashit( $this->licences->plugin->dir_path ); return substr( $path, 0, strrpos( $path, DIRECTORY_SEPARATOR ) ) . DIRECTORY_SEPARATOR; } }