From 53750a5510e694eac66930c0562c9f8eda2128c3 Mon Sep 17 00:00:00 2001 From: Nick Cernis Date: Wed, 16 Oct 2024 18:18:12 +0200 Subject: [PATCH] [LOC-6171] Fetch plugin updates from WPE servers (#223) * feat: get plugin updates from WPE Adapted from the sample code at https://github.com/wpengine/plugin-updater. * ci: comment out wp-svn usage This plugin can no longer be published on WP.org due to blocked access. * chore: bump version to 0.3.2, add changelog * ci: try newer ubuntu image Docker install is failing across the board due to a missing/old containerd dep. "The following packages have unmet dependencies: docker.io : Depends: containerd (>= 1.2.6-0ubuntu1~) E: Unable to correct problems, you have held broken packages." https://app.circleci.com/pipelines/github/studiopress/pattern-manager/1452/workflows/100cefa1-1714-484f-8fb4-1afe859ec89c/jobs/7312 * chore: ignore error_log lint * chore: use namespace --- .circleci/config.yml | 150 ++++++++------------ README.md | 5 +- lib/class-plugin-updater.php | 256 +++++++++++++++++++++++++++++++++++ pattern-manager.php | 16 ++- 4 files changed, 335 insertions(+), 92 deletions(-) create mode 100644 lib/class-plugin-updater.php diff --git a/.circleci/config.yml b/.circleci/config.yml index c9980461..800a0d5e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 orbs: node: circleci/node@5.0 - wp-svn: studiopress/wp-svn@0.2 + # wp-svn: studiopress/wp-svn@0.2 references: PLUGIN_PATH: &PLUGIN_PATH @@ -11,15 +11,10 @@ references: jobs: lint-php: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -30,15 +25,10 @@ jobs: command: sh pluginade.sh lint:php lint-js: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -50,15 +40,10 @@ jobs: sh pluginade.sh lint:js lint-css: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -70,15 +55,10 @@ jobs: sh pluginade.sh lint:css test-js: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -89,15 +69,10 @@ jobs: command: sh pluginade.sh test:js phpunit: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -110,15 +85,10 @@ jobs: sh pluginade.sh test:phpunit zip: machine: - image: ubuntu-2004:current + image: ubuntu-2204:current steps: - checkout: path: *PLUGIN_PATH - - run: - name: Install Docker - command: | - sudo apt-get update - sudo apt-get install -y docker.io - run: name: Installing Pluginade working_directory: *PLUGIN_PATH @@ -131,22 +101,22 @@ jobs: sh pluginade.sh zip - store_artifacts: path: /home/circleci/Downloads - svn-deploy: - docker: - - image: cimg/base:stable - steps: - - checkout: - path: *PLUGIN_PATH - - node/install: - node-version: '14' - - run: - name: Building the plugin - working_directory: *PLUGIN_PATH - command: | - sh pluginade.sh build - - wp-svn/deploy-plugin: - plugin-path: *PLUGIN_PATH - wporg-assets-directory: wporg + # svn-deploy: + # docker: + # - image: cimg/base:stable + # steps: + # - checkout: + # path: *PLUGIN_PATH + # - node/install: + # node-version: '14' + # - run: + # name: Building the plugin + # working_directory: *PLUGIN_PATH + # command: | + # sh pluginade.sh build + # - wp-svn/deploy-plugin: + # plugin-path: *PLUGIN_PATH + # wporg-assets-directory: wporg workflows: build: @@ -160,40 +130,40 @@ workflows: # parameters: # php-version: [ '7.4', '8.0', '8.1', '8.2' ] - zip - - wp-svn/check-versions: - filters: - tags: - only: /^\d+\.\d+\.\d+$/ - branches: - only: /^release\/.*/ - - svn-deploy: - context: genesis-svn - requires: - - wp-svn/check-versions - - lint-php - - lint-js - - lint-css - - test-js - - phpunit - filters: - tags: - only: /^\d+\.\d+\.\d+$/ - branches: - ignore: /.*/ - - approval-for-deploy-tested-up-to-bump: - type: approval - requires: - - lint-php - - lint-js - - lint-css - - test-js - - phpunit - filters: - tags: - ignore: /.*/ - branches: - only: /^bump-tested-up-to.*/ - - wp-svn/deploy-tested-up-to-bump: - context: genesis-svn - requires: - - approval-for-deploy-tested-up-to-bump + # - wp-svn/check-versions: + # filters: + # tags: + # only: /^\d+\.\d+\.\d+$/ + # branches: + # only: /^release\/.*/ + # - svn-deploy: + # context: genesis-svn + # requires: + # - wp-svn/check-versions + # - lint-php + # - lint-js + # - lint-css + # - test-js + # - phpunit + # filters: + # tags: + # only: /^\d+\.\d+\.\d+$/ + # branches: + # ignore: /.*/ + # - approval-for-deploy-tested-up-to-bump: + # type: approval + # requires: + # - lint-php + # - lint-js + # - lint-css + # - test-js + # - phpunit + # filters: + # tags: + # ignore: /.*/ + # branches: + # only: /^bump-tested-up-to.*/ + # - wp-svn/deploy-tested-up-to-bump: + # context: genesis-svn + # requires: + # - approval-for-deploy-tested-up-to-bump diff --git a/README.md b/README.md index f5b520f6..89abb5af 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Donate link: https://wpengine.com Tags: pattern, patterns, pattern design, pattern builder, block patterns Requires at least: 6.1 Tested up to: 6.3 -Stable tag: 0.3.1 +Stable tag: 0.3.2 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -114,6 +114,9 @@ Yes, you will need to have WordPress 6.1 or later installed to take advantage of ## Changelog +### 0.3.2 +* Pattern Manager now uses its own update mechanism from WP Engine servers. + ### 0.3.1 * Fix a minor security issue. diff --git a/lib/class-plugin-updater.php b/lib/class-plugin-updater.php new file mode 100644 index 00000000..8c45a5df --- /dev/null +++ b/lib/class-plugin-updater.php @@ -0,0 +1,256 @@ +api_url = 'https://wpe-plugin-updates.wpengine.com/'; + + $this->cache_time = time() + HOUR_IN_SECONDS * 5; + + $this->properties = $this->get_full_plugin_properties( $properties, $this->api_url ); + + if ( ! $this->properties ) { + return; + } + + $this->register(); + } + + /** + * Get the full plugin properties, including the directory name, version, basename, and add a transient name. + * + * @param Properties $properties These properties are passed in when instantiating to identify the plugin and it's update location. + * @param ApiUrl $api_url The URL where the api is located. + */ + public function get_full_plugin_properties( $properties, $api_url ) { + $plugins = \get_plugins(); + + // Scan through all plugins installed and find the one which matches this one in question. + foreach ( $plugins as $plugin_basename => $plugin_data ) { + // Match using the passed-in plugin's basename. + if ( $plugin_basename === $properties['plugin_basename'] ) { + // Add the values we need to the properties. + $properties['plugin_dirname'] = dirname( $plugin_basename ); + $properties['plugin_version'] = $plugin_data['Version']; + $properties['plugin_update_transient_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ); + $properties['plugin_update_transient_exp_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ) . '-expiry'; + $properties['plugin_manifest_url'] = trailingslashit( $api_url ) . trailingslashit( $properties['plugin_slug'] ) . 'info.json'; + + return $properties; + } + } + + // No matching plugin was found installed. + return null; + } + + /** + * Register hooks. + * + * @return void + */ + public function register() { + add_filter( 'plugins_api', array( $this, 'filter_plugin_update_info' ), 20, 3 ); + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'filter_plugin_update_transient' ) ); + } + + /** + * Filter the plugin update transient to take over update notifications. + * + * @param object $transient The site_transient_update_plugins transient. + * + * @handles site_transient_update_plugins + * @return object + */ + public function filter_plugin_update_transient( $transient ) { + // No update object exists. Return early. + if ( empty( $transient ) ) { + return $transient; + } + + $result = $this->fetch_plugin_info(); + + if ( false === $result ) { + return $transient; + } + + $res = $this->parse_plugin_info( $result ); + + if ( version_compare( $this->properties['plugin_version'], $result->version, '<' ) ) { + $transient->response[ $res->plugin ] = $res; + $transient->checked[ $res->plugin ] = $result->version; + } else { + $transient->no_update[ $res->plugin ] = $res; + } + + return $transient; + } + + /** + * Filters the plugin update information. + * + * @param object $res The response to be modified for the plugin in question. + * @param string $action The action in question. + * @param object $args The arguments for the plugin in question. + * + * @handles plugins_api + * @return object + */ + public function filter_plugin_update_info( $res, $action, $args ) { + // Do nothing if this is not about getting plugin information. + if ( 'plugin_information' !== $action ) { + return $res; + } + + // Do nothing if it is not our plugin. + if ( $this->properties['plugin_dirname'] !== $args->slug ) { + return $res; + } + + $result = $this->fetch_plugin_info(); + + // Do nothing if we don't get the correct response from the server. + if ( false === $result ) { + return $res; + } + + return $this->parse_plugin_info( $result ); + } + + /** + * Fetches the plugin update object from the WP Product Info API. + * + * @return object|false + */ + private function fetch_plugin_info() { + // Fetch cache first. + $expiry = get_option( $this->properties['plugin_update_transient_exp_name'], 0 ); + $response = get_option( $this->properties['plugin_update_transient_name'] ); + + if ( empty( $expiry ) || time() > $expiry || empty( $response ) ) { + $response = wp_remote_get( + $this->properties['plugin_manifest_url'], + array( + 'timeout' => 10, + 'headers' => array( + 'Accept' => 'application/json', + ), + ) + ); + + if ( + is_wp_error( $response ) || + 200 !== wp_remote_retrieve_response_code( $response ) || + empty( wp_remote_retrieve_body( $response ) ) + ) { + return false; + } + + $response = wp_remote_retrieve_body( $response ); + + // Cache the response. + update_option( $this->properties['plugin_update_transient_exp_name'], $this->cache_time, false ); + update_option( $this->properties['plugin_update_transient_name'], $response, false ); + } + + $decoded_response = json_decode( $response ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + return false; + } + + return $decoded_response; + } + + /** + * Parses the product info response into an object that WordPress would be able to understand. + * + * @param object $response The response object. + * + * @return stdClass + */ + private function parse_plugin_info( $response ) { + + global $wp_version; + + $res = new stdClass(); + $res->name = $response->name; + $res->slug = $response->slug; + $res->version = $response->version; + $res->requires = $response->requires; + $res->download_link = $response->download_link; + $res->trunk = $response->download_link; + $res->new_version = $response->version; + $res->plugin = $this->properties['plugin_basename']; + $res->package = $response->download_link; + + // Plugin information modal and core update table use a strict version comparison, which is weird. + // If we're genuinely not compatible with the point release, use our WP tested up to version. + // otherwise use exact same version as WP to avoid false positive. + $res->tested = 1 === version_compare( substr( $wp_version, 0, 3 ), $response->tested ) + ? $response->tested + : $wp_version; + + $res->sections = array( + 'description' => $response->sections->description, + 'changelog' => $response->sections->changelog, + ); + + return $res; + } +} diff --git a/pattern-manager.php b/pattern-manager.php index d32bb6ea..f8e94e37 100644 --- a/pattern-manager.php +++ b/pattern-manager.php @@ -2,7 +2,7 @@ /** * Plugin Name: Pattern Manager * Description: Create and maintain patterns. - * Version: 0.3.1 + * Version: 0.3.2 * Author: WP Engine * Author URI: wpengine.com * Text Domain: pattern-manager @@ -43,3 +43,17 @@ function include_custom_modules() { } } add_action( 'plugins_loaded', __NAMESPACE__ . '\include_custom_modules' ); + +/** + * Initialize checking of plugin updates from WP Engine. + */ +function pattern_manager_check_for_upgrades() { + $properties = array( + 'plugin_slug' => 'pattern-manager', + 'plugin_basename' => plugin_basename( __FILE__ ), + ); + + require_once __DIR__ . '/lib/class-plugin-updater.php'; + new \PatternManager\PluginUpdater( $properties ); +} +add_action( 'admin_init', __NAMESPACE__ . '\pattern_manager_check_for_upgrades' );