-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathclass-updater.php
More file actions
554 lines (484 loc) · 15.2 KB
/
class-updater.php
File metadata and controls
554 lines (484 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
<?php
/**
* Update FAIR packages.
*
* @package FAIR
*/
namespace FAIR\Updater;
use FAIR\Packages;
use Plugin_Upgrader;
use stdClass;
use Theme_Upgrader;
use TypeError;
use WP_Error;
use WP_Upgrader;
/**
* Class FAIR_Updater.
*/
class Updater {
/**
* Registered plugins.
*
* @var array<string, PluginPackage>
*/
private static array $plugins = [];
/**
* Registered themes.
*
* @var array<string, ThemePackage>
*/
private static array $themes = [];
/**
* Check if we should run on the current page.
*
* @global string $pagenow Current page.
*/
public static function should_run_on_current_page(): bool {
global $pagenow;
// Needed for mu-plugin.
if ( ! isset( $pagenow ) ) {
// phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.WP.DeprecatedFunctions.sanitize_urlFound
$php_self = isset( $_SERVER['PHP_SELF'] ) ? sanitize_url( wp_unslash( $_SERVER['PHP_SELF'] ) ) : null;
if ( null !== $php_self ) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$pagenow = basename( $php_self );
}
}
// Only run on the following pages.
$pages = [ 'update-core.php', 'update.php', 'plugins.php', 'themes.php' ];
$view_details = [ 'plugin-install.php', 'theme-install.php' ];
$autoupdate_pages = [ 'admin-ajax.php', 'index.php', 'wp-cron.php' ];
if ( ! in_array( $pagenow, array_merge( $pages, $view_details, $autoupdate_pages ), true ) ) {
return false;
}
return true;
}
/**
* Load hooks.
*
* @return void
*/
public static function load_hooks() {
add_filter( 'upgrader_source_selection', [ __CLASS__, 'upgrader_source_selection' ], 10, 4 );
add_filter( 'plugins_api', [ __CLASS__, 'plugin_api_details' ], 99, 3 );
add_filter( 'themes_api', [ __CLASS__, 'theme_api_details' ], 99, 3 );
add_filter( 'site_transient_update_plugins', [ __CLASS__, 'handle_update_plugins_transient' ], 20, 1 );
add_filter( 'site_transient_update_themes', [ __CLASS__, 'handle_update_themes_transient' ], 20, 1 );
if ( ! is_multisite() ) {
add_filter( 'wp_prepare_themes_for_js', [ __CLASS__, 'customize_theme_update_html' ] );
}
/**
* Filter whether to verify FAIR package signatures during update.
*
* @param bool $verify Whether to verify signatures. Default true.
* @return bool
*/
if ( apply_filters( 'fair.packages.updater.verify_signatures', true ) ) {
add_filter( 'upgrader_pre_download', 'FAIR\\Updater\\verify_signature_on_download', 10, 4 );
}
add_filter( 'upgrader_source_selection', 'FAIR\\Updater\\verify_did_on_source_selection', 9, 4 );
foreach ( self::$plugins as $package ) {
Packages\add_package_to_release_cache( $package->did );
}
foreach ( self::$themes as $package ) {
Packages\add_package_to_release_cache( $package->did );
}
}
/**
* Correctly rename dependency for activation.
*
* @param string|WP_Error $source Path of $source, or a WP_Error object.
* @param string $remote_source Path of $remote_source.
* @param WP_Upgrader $upgrader An Upgrader object.
* @param array $hook_extra Array of hook data.
*
* @throws TypeError If the type of $upgrader is not correct.
*
* @return string|WP_Error
*/
public static function upgrader_source_selection( $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) {
global $wp_filesystem;
// Exit early for errors.
if ( is_wp_error( $source ) ) {
return $source;
}
$new_source = $source;
// Exit if installing.
if ( isset( $hook_extra['action'] ) && 'install' === $hook_extra['action'] ) {
return $source;
}
if ( ! $upgrader instanceof Plugin_Upgrader && ! $upgrader instanceof Theme_Upgrader ) {
throw new TypeError( __METHOD__ . '(): Argument #3 ($upgrader) must be of type Plugin_Upgrader|Theme_Upgrader, ' . esc_attr( gettype( $upgrader ) ) . ' given.' );
}
// Rename plugins.
if ( $upgrader instanceof Plugin_Upgrader ) {
if ( ! isset( $hook_extra['plugin'] ) ) {
return $source;
}
$slug = dirname( $hook_extra['plugin'] );
$new_source = trailingslashit( $remote_source ) . $slug;
}
// Rename themes.
if ( $upgrader instanceof Theme_Upgrader ) {
if ( ! isset( $hook_extra['theme'] ) ) {
return $source;
}
$slug = $hook_extra['theme'];
$new_source = trailingslashit( $remote_source ) . $slug;
}
if ( basename( $source ) === $slug ) {
return $source;
}
if ( trailingslashit( strtolower( $source ) ) !== trailingslashit( strtolower( $new_source ) ) ) {
$wp_filesystem->move( $source, $new_source, true );
}
return trailingslashit( $new_source );
}
/**
* Put changelog in plugins_api, return WP.org data as appropriate
*
* @param bool $result Default false.
* @param string $action The type of information being requested from the Plugin Installation API.
* @param stdClass $response Repo API arguments.
*
* @return stdClass|bool
*/
public static function plugin_api_details( $result, string $action, stdClass $response ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
return self::handle_plugin_api( $result, $response->slug ?? '' );
}
/**
* Put changelog in themes_api, return WP.org data as appropriate
*
* @param bool $result Default false.
* @param string $action The type of information being requested from the Theme Installation API.
* @param stdClass $response Repo API arguments.
*
* @return stdClass|bool
*/
public static function theme_api_details( $result, string $action, stdClass $response ) {
if ( 'theme_information' !== $action ) {
return $result;
}
return self::handle_theme_api( $result, $response->slug ?? '' );
}
/**
* Find a package by its API slug.
*
* @param bool|object $result The result object or false.
* @param string $slug The package slug.
* @param Package[] $packages The packages to search.
* @return bool|object The result.
*/
private static function find_package_by_api_slug( $result, string $slug, array $packages ) {
if ( empty( $slug ) ) {
return $result;
}
foreach ( $packages as $package ) {
$metadata = $package->get_metadata();
if ( is_wp_error( $metadata ) || ! $metadata ) {
continue;
}
// Check if slug matches (with or without DID hash suffix).
$slug_arr = [ $metadata->slug, $metadata->slug . '-' . Packages\get_did_hash( $package->did ) ];
if ( in_array( $slug, $slug_arr, true ) ) {
return (object) Packages\get_package_data( $package->did );
}
}
return $result;
}
/**
* Handle site_transient_update_plugins filter.
*
* @param stdClass $transient Plugin|Theme update transient.
* @return stdClass The modified transient.
*/
public static function handle_update_plugins_transient( $transient ) {
$transient = self::update_site_transient( $transient, self::$plugins );
// WordPress expects plugin responses as objects.
foreach ( $transient->response ?? [] as $key => $value ) {
$transient->response[ $key ] = (object) $value;
}
foreach ( $transient->no_update ?? [] as $key => $value ) {
$transient->no_update[ $key ] = (object) $value;
}
return $transient;
}
/**
* Handle site_transient_update_themes filter.
*
* @param stdClass $transient Plugin|Theme update transient.
* @return stdClass The modified transient.
*/
public static function handle_update_themes_transient( $transient ) {
return self::update_site_transient( $transient, self::$themes );
}
/**
* Hook into site_transient_update_{plugins|themes} to update from GitHub.
*
* @param stdClass $transient Plugin|Theme update transient.
* @param array<string, Package> $packages Array of packages to process.
* @return stdClass
*/
private static function update_site_transient( $transient, array $packages ) {
// needed to fix PHP 7.4 warning.
if ( ! is_object( $transient ) ) {
$transient = new stdClass();
}
foreach ( $packages as $package ) {
if ( empty( $package->filepath ) || empty( $package->local_version ) ) {
continue;
}
$release = $package->get_release();
if ( is_wp_error( $release ) || ! $release ) {
continue;
}
$response = Packages\get_package_data( $package->did );
if ( is_wp_error( $response ) ) {
continue;
}
$rel_path = $package->get_relative_path();
$response['slug'] = $response['slug_didhash'];
$is_compatible = Packages\check_requirements( $release );
if ( $is_compatible && version_compare( $release->version, $package->local_version, '>' ) ) {
$transient->response[ $rel_path ] = $response;
} else {
// Add repo without update to $transient->no_update for 'View details' link.
$transient->no_update[ $rel_path ] = $response;
}
}
return $transient;
}
/**
* Call theme messaging for single site installation.
*
* @author Seth Carstens
*
* @param array $prepared_themes Array of prepared themes.
*
* @return array
*/
public static function customize_theme_update_html( $prepared_themes ) {
foreach ( self::$themes as $package ) {
$theme = $package->get_metadata();
if ( is_wp_error( $theme ) || ! $theme ) {
continue;
}
if ( ! isset( $prepared_themes[ $theme->slug ] ) ) {
continue;
}
if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) {
$prepared_themes[ $theme->slug ]['update'] = self::append_theme_actions_content( $theme );
} else {
$prepared_themes[ $theme->slug ]['description'] .= self::append_theme_actions_content( $theme );
}
}
return $prepared_themes;
}
/**
* Create theme update messaging for single site installation.
*
* @author Seth Carstens
*
* @access protected
* @codeCoverageIgnore
*
* @param stdClass $theme Theme object.
*
* @return string (content buffer)
*/
private static function append_theme_actions_content( $theme ) {
$details_url = esc_attr(
add_query_arg(
[
'tab' => 'theme-information',
'theme' => $theme->slug,
'TB_iframe' => 'true',
'width' => 270,
'height' => 400,
],
self_admin_url( 'theme-install.php' )
)
);
$nonced_update_url = wp_nonce_url(
esc_attr(
add_query_arg(
[
'action' => 'upgrade-theme',
'theme' => rawurlencode( $theme->slug ),
],
self_admin_url( 'update.php' )
)
),
'upgrade-theme_' . $theme->slug
);
$current = get_site_transient( 'update_themes' );
/**
* Display theme update links.
*/
ob_start();
if ( isset( $current->response[ $theme->slug ] ) ) {
?>
<p>
<strong>
<?php
printf(
/* translators: %s: theme name */
esc_html__( 'There is a new version of %s available.', 'fair' ),
esc_attr( $theme->name )
);
printf(
' <a href="%s" class="thickbox open-plugin-details-modal" title="%s">',
esc_url( $details_url ),
esc_attr( $theme->name )
);
if ( ! empty( $current->response[ $theme->slug ]['package'] ) ) {
printf(
/* translators: 1: opening anchor with version number, 2: closing anchor tag, 3: opening anchor with update URL */
esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'fair' ),
$theme->remote_version = isset( $theme->remote_version ) ? esc_attr( $theme->remote_version ) : null,
'</a>',
sprintf(
/* translators: %s: theme name */
'<a aria-label="' . esc_attr__( '%s: update now', 'fair' ) . '" id="update-theme" data-slug="' . esc_attr( $theme->slug ) . '" href="' . esc_url( $nonced_update_url ) . '">',
esc_attr( $theme->name )
)
);
} else {
printf(
/* translators: 1: opening anchor with version number, 2: closing anchor tag, 3: opening anchor with update URL */
esc_html__( 'View version %1$s details%2$s.', 'fair' ),
$theme->remote_version = isset( $theme->remote_version ) ? esc_attr( $theme->remote_version ) : null,
'</a>'
);
echo(
'<p><i>' . esc_html__( 'Automatic update is unavailable for this theme.', 'fair' ) . '</i></p>'
);
}
?>
</strong>
</p>
<?php
}
return trim( ob_get_clean(), '1' );
}
/**
* Handle plugin API requests.
*
* @param bool|object $result The result object or false.
* @param string $slug The plugin slug.
* @return bool|object The result.
*/
private static function handle_plugin_api( $result, string $slug ) {
return self::find_package_by_api_slug( $result, $slug, self::$plugins );
}
/**
* Handle theme API requests.
*
* @param bool|object $result The result object or false.
* @param string $slug The theme slug.
* @return bool|object The result.
*/
private static function handle_theme_api( $result, string $slug ) {
return self::find_package_by_api_slug( $result, $slug, self::$themes );
}
/**
* Register a plugin with the registry.
*
* @param string $did The DID of the plugin.
* @param string $filepath Absolute path to the main plugin file.
*/
public static function register_plugin( string $did, string $filepath ): void {
self::$plugins[ $did ] = new PluginPackage( $did, $filepath );
}
/**
* Register a theme with the registry.
*
* @param string $did The DID of the theme.
* @param string $filepath Absolute path to the theme's style.css file.
*/
public static function register_theme( string $did, string $filepath ): void {
self::$themes[ $did ] = new ThemePackage( $did, $filepath );
}
/**
* Get a plugin by DID.
*
* @param string $did The DID to look up.
*/
public static function get_plugin( string $did ): ?PluginPackage {
return self::$plugins[ $did ] ?? null;
}
/**
* Get a theme by DID.
*
* @param string $did The DID to look up.
*/
public static function get_theme( string $did ): ?ThemePackage {
return self::$themes[ $did ] ?? null;
}
/**
* Get all registered plugins.
*
* @return array<string, PluginPackage> All registered plugins.
*/
public static function get_plugins(): array {
return self::$plugins;
}
/**
* Get all registered themes.
*
* @return array<string, ThemePackage> All registered themes.
*/
public static function get_themes(): array {
return self::$themes;
}
/**
* Find a plugin by the plugin file path (relative to plugins directory).
*
* @param string $plugin_file Plugin file path relative to plugins directory (e.g., 'my-plugin/my-plugin.php').
*/
public static function get_plugin_by_file( string $plugin_file ): ?PluginPackage {
$plugin_path = trailingslashit( WP_PLUGIN_DIR ) . $plugin_file;
foreach ( self::$plugins as $package ) {
if ( $package->filepath === $plugin_path ) {
return $package;
}
}
return null;
}
/**
* Find a plugin by its slug.
*
* @param string $slug The plugin directory name.
*/
public static function get_plugin_by_slug( string $slug ): ?PluginPackage {
foreach ( self::$plugins as $package ) {
if ( $package->get_slug() === $slug ) {
return $package;
}
}
return null;
}
/**
* Find a theme by its slug.
*
* @param string $slug The theme stylesheet.
*/
public static function get_theme_by_slug( string $slug ): ?ThemePackage {
foreach ( self::$themes as $package ) {
if ( $package->get_slug() === $slug ) {
return $package;
}
}
return null;
}
/**
* Reset the registry.
*/
public static function reset(): void {
self::$plugins = [];
self::$themes = [];
}
}