Skip to content

Commit 11b4819

Browse files
authored
[LOC-6170] Fetch plugin updates from WPE servers (#31)
* 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. We'll just lint in Circle for now. * chore: add npm run zip script To make it easier to package a zip for production. * chore: bump version to 1.1.2, add changelog * chore: fix require path * fix: deprecation warnings due to use of dynamic class properties Properties need to be declared before use as of PHP 8.2. https://php.watch/versions/8.2/dynamic-properties-deprecated * fix: prevent deprecation notices from more missing properties Deprecated: Creation of dynamic property SEO_Data_Transporter_Admin::$analysis_result is deprecated in /Users/user.name/Local Sites/genesispluginupdater/app/public/wp-content/plugins/seo-data-transporter/includes/class-seo-data-transporter-admin.php on line 181
1 parent f3ae056 commit 11b4819

13 files changed

+536
-101
lines changed

.circleci/config.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: 2.1
22

33
orbs:
44
php: circleci/php@1.1
5-
wp-svn: studiopress/wp-svn@0.1
5+
# wp-svn: studiopress/wp-svn@0.1
66

77
jobs:
88
lint:
@@ -30,16 +30,16 @@ workflows:
3030
test-deploy:
3131
jobs:
3232
- lint
33-
- approval-for-deploy-tested-up-to-bump:
34-
requires:
35-
- lint
36-
type: approval
37-
filters:
38-
tags:
39-
ignore: /.*/
40-
branches:
41-
only: /^bump-tested-up-to.*/
42-
- wp-svn/deploy-tested-up-to-bump:
43-
context: genesis-svn
44-
requires:
45-
- approval-for-deploy-tested-up-to-bump
33+
# - approval-for-deploy-tested-up-to-bump:
34+
# requires:
35+
# - lint
36+
# type: approval
37+
# filters:
38+
# tags:
39+
# ignore: /.*/
40+
# branches:
41+
# only: /^bump-tested-up-to.*/
42+
# - wp-svn/deploy-tested-up-to-bump:
43+
# context: genesis-svn
44+
# requires:
45+
# - approval-for-deploy-tested-up-to-bump

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ Thumbs.db
33
.svn
44
node_modules/
55
vendor/
6+
*.zip

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

.svnignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1+
.editorconfig
12
.git
23
.gitignore
34
.gitattributes
45
.svnignore
6+
.DS_Store
7+
.nvmrc
8+
.circleci/
9+
package.json
10+
package-lock.json
11+
composer.json
12+
composer.lock
513
node_modules
14+
vendor/
15+
scripts/
16+
*.zip

includes/class-seo-data-transporter-admin.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,34 @@ class SEO_Data_Transporter_Admin {
3535
*/
3636
private $plugins;
3737

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

0 commit comments

Comments
 (0)