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
|
<?php /** * Fetches and formats data for AMP reader themes. * * @package AMP * @since 2.0 */
namespace AmpProject\AmpWP\Admin;
use AMP_Core_Theme_Sanitizer; use AMP_Options_Manager; use AmpProject\AmpWP\ExtraThemeAndPluginHeaders; use AmpProject\AmpWP\Option; use WP_Error; use WP_Theme; use WP_Upgrader;
/** * Handles reader themes. * * @since 2.0 * @internal */ final class ReaderThemes { /** * Formatted theme data. * * @var array */ private $themes;
/** * Reader themes supported by default. * * @var array */ private $default_reader_themes;
/** * Whether themes can be installed in the current WordPress installation. * * @var bool */ private $can_install_themes;
/** * The error resulting from a failed themes_api request. * * @var null|WP_Error */ private $themes_api_error;
/** * The default reader theme. * * @var string */ const DEFAULT_READER_THEME = 'legacy';
/** * Status indicating a reader theme is active on the site. * * @var string */ const STATUS_ACTIVE = 'active';
/** * Status indicating a reader theme is installed but not active. * * @var string */ const STATUS_INSTALLED = 'installed';
/** * Status indicating a reader theme is not installed but is installable. * * @var string */ const STATUS_INSTALLABLE = 'installable';
/** * Status indicating a reader theme is not installed and can't be installed. * * @var string */ const STATUS_NON_INSTALLABLE = 'non-installable';
/** * Retrieves all AMP plugin options specified in the endpoint schema. * * @return array Formatted theme data. */ public function get_themes() { if ( null !== $this->themes ) { return $this->themes; }
$themes = $this->get_default_reader_themes();
// Also include themes that declare AMP-compatibility in their style.css. $default_reader_theme_slugs = wp_list_pluck( $themes, 'slug' ); foreach ( $this->get_compatible_installed_themes() as $compatible_installed_theme ) { if ( ! in_array( $compatible_installed_theme->get_stylesheet(), $default_reader_theme_slugs, true ) ) { $themes[] = $this->normalize_theme_data( $compatible_installed_theme ); } }
/** * Filters supported reader themes. * * @param array $themes [ * Reader theme data. * { * @type string $name Theme name. * @type string $slug Theme slug. * @type string $slug URL of theme preview. * @type string $screenshot_url The URL of a mobile screenshot. Note: if this is empty, the theme may not display. * @type string $homepage A link to a page with more information about the theme. * @type string $description A description of the theme. * @type string|boolean $requires Minimum version of WordPress required by the theme. False if all versions are supported. * @type string|boolean $requires_php Minimum version of PHP required by the theme. False if all versions are supported. * @type string $download_link A link to the theme's zip file. If empty, the plugin will attempt to download the theme from wordpress.org. * } * ] */ $themes = (array) apply_filters( 'amp_reader_themes', $themes );
$selected_theme_slug = AMP_Options_Manager::get_option( Option::READER_THEME ); $theme_slugs = wp_list_pluck( $themes, 'slug' );
/* * Check if the chosen Reader theme is among the list of filtered themes. If not, an attempt will be made to * obtain the theme data from the list of installed themes. If neither case is true, the AMP Legacy theme will * be used as a fallback. */ if ( self::DEFAULT_READER_THEME !== $selected_theme_slug && ! in_array( $selected_theme_slug, $theme_slugs, true ) ) { $active_theme = wp_get_theme( $selected_theme_slug );
if ( $active_theme->exists() ) { $themes[] = $this->normalize_theme_data( $active_theme ); } }
$themes = array_filter( $themes, static function( $theme ) { return is_array( $theme ) && ! empty( $theme['slug'] ); } );
$themes = array_map( function ( $theme ) { $theme = $this->normalize_theme_data( $theme ); $theme['availability'] = $this->get_theme_availability( $theme ); return $theme; }, $themes );
// Sort themes alphabetically before AMP Legacy. usort( $themes, static function ( $a, $b ) { return strcmp( $a['name'], $b['name'] ); } );
/* * Append the AMP Legacy theme details after filtering the default themes. This ensures the AMP Legacy theme * will always be available as a fallback if the chosen Reader theme becomes unavailable. */ $themes[] = $this->get_legacy_theme();
$this->themes = array_values( $themes );
return $this->themes; }
/** * Provides the themes api error, or null if there is no error. * * @return null|WP_Error */ public function get_themes_api_error() { return $this->themes_api_error; }
/** * Gets a reader theme by slug. * * @param string $slug Theme slug. * @return array|false Theme data or false if the theme is not found. */ public function get_reader_theme_by_slug( $slug ) { return current( array_filter( $this->get_themes(), static function ( $theme ) use ( $slug ) { return $theme['slug'] === $slug; } ) ); }
/** * Retrieves theme data. * * @return array Theme data from the wordpress.org API, or an empty array on failure. */ public function get_default_reader_themes() { if ( null !== $this->default_reader_themes ) { return $this->default_reader_themes; }
$cache_key = 'amp_themes_wporg'; $response = get_transient( $cache_key );
if ( ! $response ) { require_once ABSPATH . 'wp-admin/includes/theme.php';
$response = themes_api( 'query_themes', [ 'author' => 'wordpressdotorg', 'per_page' => 24, // There are only 13 as of 12/2020. ] );
if ( is_array( $response ) ) { $response = (object) $response; }
/** * The response must minimally be an object with a themes array. * * @see https://wordpress.org/support/topic/issue-during-activating-the-updated-plugins/#post-13383737 */ if ( ! is_object( $response ) || ! property_exists( $response, 'themes' ) || ! is_array( $response->themes ) || is_wp_error( $response ) ) { $message = __( 'The request for reader themes from WordPress.org resulted in an invalid response. Check your Site Health to confirm that your site can communicate with WordPress.org. Otherwise, please try again later or contact your host.', 'amp' ); if ( is_wp_error( $response ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { $message .= ' ' . __( 'Error:', 'amp' ); if ( $response->get_error_message() ) { $message .= sprintf( ' %s (%s).', $response->get_error_message(), $response->get_error_code() ); } else { $message .= ' ' . $response->get_error_code() . '.'; } }
$this->themes_api_error = new WP_Error( 'amp_themes_api_invalid_response', $message );
return []; }
if ( empty( $response->themes ) ) { $this->themes_api_error = new WP_Error( 'amp_themes_api_empty_themes_array', __( 'The default reader themes cannot be displayed because a plugin appears to be overriding the themes response from WordPress.org.', 'amp' ) ); return []; }
// Store the transient only if the response was valid. set_transient( $cache_key, $response, DAY_IN_SECONDS ); }
$supported_themes = array_diff( AMP_Core_Theme_Sanitizer::get_supported_themes(), [ 'twentyten' ] // Because it is not responsive. );
// Get the subset of themes. $reader_themes = array_filter( $response->themes, static function ( $theme ) use ( $supported_themes ) { return in_array( $theme->slug, $supported_themes, true ); } );
$reader_themes = array_map( function ( $theme ) { $theme_data = $this->normalize_theme_data( $theme ); $theme_data['screenshot_url'] = amp_get_asset_url( "images/reader-themes/{$theme_data['slug']}.jpg" );
return $theme_data; }, $reader_themes );
$this->default_reader_themes = $reader_themes; return $this->default_reader_themes; }
/** * Get installed themes that are marked as being AMP-compatible. * * @return WP_Theme[] Themes. */ private function get_compatible_installed_themes() { $compatible_themes = []; foreach ( wp_get_themes() as $theme ) { $value = $theme->get( ExtraThemeAndPluginHeaders::AMP_HEADER ); if ( rest_sanitize_boolean( $value ) && ExtraThemeAndPluginHeaders::AMP_HEADER_LEGACY !== $value ) { $compatible_themes[] = $theme; } } return $compatible_themes; }
/** * Normalize the specified theme data. * * @param WP_Theme|array|\stdClass $theme Theme. * @return array Normalized theme data. */ private function normalize_theme_data( $theme ) { if ( $theme instanceof WP_Theme ) { if ( $theme->errors() ) { return []; }
$mobile_screenshot = null; if ( file_exists( $theme->get_stylesheet_directory() . '/screenshot-mobile.png' ) ) { $mobile_screenshot = $theme->get_stylesheet_directory_uri() . '/screenshot-mobile.png'; } elseif ( file_exists( $theme->get_stylesheet_directory() . '/screenshot-mobile.jpg' ) ) { $mobile_screenshot = $theme->get_stylesheet_directory_uri() . '/screenshot-mobile.jpg'; } else { $mobile_screenshot = $theme->get_screenshot(); } if ( $mobile_screenshot ) { $mobile_screenshot = add_query_arg( 'ver', $theme->get( 'Version' ), $mobile_screenshot ); }
return [ 'name' => $theme->display( 'Name' ) ?: $theme->get_stylesheet(), 'slug' => $theme->get_stylesheet(), 'preview_url' => null, 'screenshot_url' => $mobile_screenshot ?: '', 'homepage' => $theme->display( 'ThemeURI' ), 'description' => $theme->display( 'Description' ), 'requires' => $theme->get( 'RequiresWP' ), 'requires_php' => $theme->get( 'RequiresPHP' ), ]; }
if ( ! is_array( $theme ) && ! is_object( $theme ) ) { return []; }
$keys = [ 'name', 'slug', 'preview_url', 'screenshot_url', 'homepage', 'description', 'requires', 'requires_php', ];
return array_merge( array_fill_keys( $keys, '' ), wp_array_slice_assoc( (array) $theme, $keys ) ); }
/** * Returns whether a theme can be installed on the system. * * @param array $theme Theme data. * @return bool True if themes can be installed. */ public function can_install_theme( $theme ) { if ( ! current_user_can( 'install_themes' ) ) { return false; }
if ( null === $this->can_install_themes ) { if ( ! function_exists( 'request_filesystem_credentials' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; }
if ( ! class_exists( 'WP_Upgrader' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; }
ob_start(); // Prevent request_filesystem_credentials() from outputting the request-filesystem-credentials-form. require_once ABSPATH . 'wp-admin/includes/template.php'; // Needed for submit_button(). $this->can_install_themes = true === ( new WP_Upgrader() )->fs_connect( [ get_theme_root() ] ); ob_end_clean(); }
if ( ! $this->can_install_themes ) { return false; }
if ( ! empty( $theme['requires'] ) && version_compare( get_bloginfo( 'version' ), $theme['requires'], '<' ) ) { return false; }
if ( ! empty( $theme['requires_php'] ) && version_compare( phpversion(), $theme['requires_php'], '<' ) ) { return false; }
return true; }
/** * Returns reader theme availability status. * * @param array $theme Theme data. * @return string Theme availability status. */ public function get_theme_availability( $theme ) { switch ( true ) { case get_stylesheet() === $theme['slug']: return self::STATUS_ACTIVE;
case self::DEFAULT_READER_THEME === $theme['slug'] || wp_get_theme( $theme['slug'] )->exists(): return self::STATUS_INSTALLED;
case $this->can_install_theme( $theme ): return self::STATUS_INSTALLABLE;
default: return self::STATUS_NON_INSTALLABLE; } }
/** * Determine if the data for the specified Reader theme exists. * * @param string $theme_slug Theme slug. * @return bool Whether the Reader theme data exists. */ public function theme_data_exists( $theme_slug ) { return in_array( $theme_slug, wp_list_pluck( $this->get_themes(), 'slug' ), true ); }
/** * Determine if the AMP legacy Reader theme is being used as a fallback. * * @return bool True if being used as a fallback, false otherwise. */ public function using_fallback_theme() { return amp_is_legacy() && self::DEFAULT_READER_THEME !== AMP_Options_Manager::get_option( Option::READER_THEME ); }
/** * Provides details for the legacy theme included with the plugin. * * @return array */ private function get_legacy_theme() { return [ 'name' => __( 'AMP Legacy', 'amp' ), 'slug' => self::DEFAULT_READER_THEME, 'preview_url' => 'https://amp-wp.org', // This is unused. 'screenshot_url' => amp_get_asset_url( 'images/reader-themes/legacy.jpg' ), 'homepage' => 'https://amp-wp.org/documentation/how-the-plugin-works/classic-templates/', 'description' => __( 'The original templates included in the plugin with limited customization options.', 'amp' ), 'requires' => false, 'requires_php' => false, 'availability' => self::STATUS_INSTALLED, ]; } }
|