|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * The PluginUpdater class which can be used to pull plugin updates from a new location. |
| 4 | + * @package your-plugin-package-name |
| 5 | + */ |
| 6 | + |
| 7 | +namespace WPEngine_PHPCompat; |
| 8 | + |
| 9 | +// Exit if accessed directly. |
| 10 | +if ( ! defined( 'ABSPATH' ) ) { |
| 11 | + exit; |
| 12 | +} |
| 13 | + |
| 14 | +use stdClass; |
| 15 | + |
| 16 | +/** |
| 17 | + * The PluginUpdater class which can be used to pull plugin updates from a new location. |
| 18 | + */ |
| 19 | +class PluginUpdater { |
| 20 | + /** |
| 21 | + * The URL where the api is located. |
| 22 | + * @var string |
| 23 | + */ |
| 24 | + private $api_url; |
| 25 | + |
| 26 | + /** |
| 27 | + * The amount of time to wait before checking for new updates. |
| 28 | + * @var int |
| 29 | + */ |
| 30 | + private $cache_time; |
| 31 | + |
| 32 | + /** |
| 33 | + * These properties are passed in when instantiating to identify the plugin and it's update location. |
| 34 | + * @var array |
| 35 | + */ |
| 36 | + private $properties; |
| 37 | + |
| 38 | + /** |
| 39 | + * Get the class constructed. |
| 40 | + * |
| 41 | + * @param array $properties These properties are passed in when instantiating to identify the plugin and it's update location. |
| 42 | + */ |
| 43 | + public function __construct( $properties ) { |
| 44 | + if ( |
| 45 | + empty( $properties['plugin_slug'] ) || |
| 46 | + empty( $properties['plugin_basename'] ) |
| 47 | + ) { |
| 48 | + error_log( 'WPE Secure Plugin Updater received a malformed request.' ); |
| 49 | + return; |
| 50 | + } |
| 51 | + |
| 52 | + $this->api_url = 'https://wpe-plugin-updates.wpengine.com/'; |
| 53 | + |
| 54 | + $this->cache_time = time() + HOUR_IN_SECONDS * 5; |
| 55 | + |
| 56 | + $this->properties = $this->get_full_plugin_properties( $properties, $this->api_url ); |
| 57 | + |
| 58 | + if ( ! $this->properties ) { |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + $this->register(); |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * Get the full plugin properties, including the directory name, version, basename, and add a transient name. |
| 67 | + * |
| 68 | + * @param array $properties These properties are passed in when instantiating to identify the plugin and it's update location. |
| 69 | + * @param int $api_url The URL where the api is located. |
| 70 | + */ |
| 71 | + public function get_full_plugin_properties( $properties, $api_url ) { |
| 72 | + $plugins = \get_plugins(); |
| 73 | + |
| 74 | + // Scan through all plugins installed and find the one which matches this one in question. |
| 75 | + foreach ( $plugins as $plugin_basename => $plugin_data ) { |
| 76 | + // Match using the passed-in plugin's basename. |
| 77 | + if ( $plugin_basename === $properties['plugin_basename'] ) { |
| 78 | + // Add the values we need to the properties. |
| 79 | + $properties['plugin_dirname'] = dirname( $plugin_basename ); |
| 80 | + $properties['plugin_version'] = $plugin_data['Version']; |
| 81 | + $properties['plugin_update_transient_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ); |
| 82 | + $properties['plugin_update_transient_exp_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ) . '-expiry'; |
| 83 | + $properties['plugin_manifest_url'] = trailingslashit( $api_url ) . trailingslashit( $properties['plugin_slug'] ) . 'info.json'; |
| 84 | + |
| 85 | + return $properties; |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + // No matching plugin was found installed. |
| 90 | + return null; |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + * Register hooks. |
| 95 | + * |
| 96 | + * @return void |
| 97 | + */ |
| 98 | + public function register() { |
| 99 | + add_filter( 'plugins_api', array( $this, 'filter_plugin_update_info' ), 20, 3 ); |
| 100 | + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'filter_plugin_update_transient' ) ); |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Filter the plugin update transient to take over update notifications. |
| 105 | + * |
| 106 | + * @param ?object $transient_value The value of the `site_transient_update_plugins` transient. |
| 107 | + * |
| 108 | + * @handles site_transient_update_plugins |
| 109 | + * @return object|null |
| 110 | + */ |
| 111 | + public function filter_plugin_update_transient( $transient_value ) { |
| 112 | + // No update object exists. Return early. |
| 113 | + if ( empty( $transient_value ) ) { |
| 114 | + return $transient_value; |
| 115 | + } |
| 116 | + |
| 117 | + $result = $this->fetch_plugin_info(); |
| 118 | + |
| 119 | + if ( false === $result ) { |
| 120 | + return $transient_value; |
| 121 | + } |
| 122 | + |
| 123 | + $res = $this->parse_plugin_info( $result ); |
| 124 | + |
| 125 | + if ( version_compare( $this->properties['plugin_version'], $result->version, '<' ) ) { |
| 126 | + $transient_value->response[ $res->plugin ] = $res; |
| 127 | + $transient_value->checked[ $res->plugin ] = $result->version; |
| 128 | + } else { |
| 129 | + $transient_value->no_update[ $res->plugin ] = $res; |
| 130 | + } |
| 131 | + |
| 132 | + return $transient_value; |
| 133 | + } |
| 134 | + |
| 135 | + /** |
| 136 | + * Filters the plugin update information. |
| 137 | + * |
| 138 | + * @param object $res The response to be modified for the plugin in question. |
| 139 | + * @param string $action The action in question. |
| 140 | + * @param object $args The arguments for the plugin in question. |
| 141 | + * |
| 142 | + * @handles plugins_api |
| 143 | + * @return object |
| 144 | + */ |
| 145 | + public function filter_plugin_update_info( $res, $action, $args ) { |
| 146 | + // Do nothing if this is not about getting plugin information. |
| 147 | + if ( 'plugin_information' !== $action ) { |
| 148 | + return $res; |
| 149 | + } |
| 150 | + |
| 151 | + // Do nothing if it is not our plugin. |
| 152 | + if ( $this->properties['plugin_dirname'] !== $args->slug ) { |
| 153 | + return $res; |
| 154 | + } |
| 155 | + |
| 156 | + $result = $this->fetch_plugin_info(); |
| 157 | + |
| 158 | + // Do nothing if we don't get the correct response from the server. |
| 159 | + if ( false === $result ) { |
| 160 | + return $res; |
| 161 | + } |
| 162 | + |
| 163 | + return $this->parse_plugin_info( $result ); |
| 164 | + } |
| 165 | + |
| 166 | + /** |
| 167 | + * Fetches the plugin update object from the WP Product Info API. |
| 168 | + * |
| 169 | + * @return object|false |
| 170 | + */ |
| 171 | + private function fetch_plugin_info() { |
| 172 | + // Fetch cache first. |
| 173 | + $expiry = get_option( $this->properties['plugin_update_transient_exp_name'], 0 ); |
| 174 | + $response = get_option( $this->properties['plugin_update_transient_name'] ); |
| 175 | + |
| 176 | + if ( empty( $expiry ) || time() > $expiry || empty( $response ) ) { |
| 177 | + $response = wp_remote_get( |
| 178 | + $this->properties['plugin_manifest_url'], |
| 179 | + array( |
| 180 | + 'timeout' => 10, |
| 181 | + 'headers' => array( |
| 182 | + 'Accept' => 'application/json', |
| 183 | + ), |
| 184 | + ) |
| 185 | + ); |
| 186 | + |
| 187 | + if ( |
| 188 | + is_wp_error( $response ) || |
| 189 | + 200 !== wp_remote_retrieve_response_code( $response ) || |
| 190 | + empty( wp_remote_retrieve_body( $response ) ) |
| 191 | + ) { |
| 192 | + return false; |
| 193 | + } |
| 194 | + |
| 195 | + $response = wp_remote_retrieve_body( $response ); |
| 196 | + |
| 197 | + // Cache the response. |
| 198 | + update_option( $this->properties['plugin_update_transient_exp_name'], $this->cache_time, false ); |
| 199 | + update_option( $this->properties['plugin_update_transient_name'], $response, false ); |
| 200 | + } |
| 201 | + |
| 202 | + $decoded_response = json_decode( $response ); |
| 203 | + |
| 204 | + if ( json_last_error() !== JSON_ERROR_NONE ) { |
| 205 | + return false; |
| 206 | + } |
| 207 | + |
| 208 | + return $decoded_response; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * Parses the product info response into an object that WordPress would be able to understand. |
| 213 | + * |
| 214 | + * @param object $response The response object. |
| 215 | + * |
| 216 | + * @return stdClass |
| 217 | + */ |
| 218 | + private function parse_plugin_info( $response ) { |
| 219 | + |
| 220 | + global $wp_version; |
| 221 | + |
| 222 | + $res = new stdClass(); |
| 223 | + $res->name = $response->name; |
| 224 | + $res->slug = $response->slug; |
| 225 | + $res->version = $response->version; |
| 226 | + $res->requires = $response->requires; |
| 227 | + $res->download_link = $response->download_link; |
| 228 | + $res->trunk = $response->download_link; |
| 229 | + $res->new_version = $response->version; |
| 230 | + $res->plugin = $this->properties['plugin_basename']; |
| 231 | + $res->package = $response->download_link; |
| 232 | + |
| 233 | + // Plugin information modal and core update table use a strict version comparison, which is weird. |
| 234 | + // If we're genuinely not compatible with the point release, use our WP tested up to version. |
| 235 | + // otherwise use exact same version as WP to avoid false positive. |
| 236 | + $res->tested = 1 === version_compare( substr( $wp_version, 0, 3 ), $response->tested ) |
| 237 | + ? $response->tested |
| 238 | + : $wp_version; |
| 239 | + |
| 240 | + $res->sections = array( |
| 241 | + 'description' => $response->sections->description, |
| 242 | + 'changelog' => $response->sections->changelog, |
| 243 | + ); |
| 244 | + |
| 245 | + return $res; |
| 246 | + } |
| 247 | +} |
0 commit comments