PluginProbe
Performance Lab / 3.6.1
Performance Lab v3.6.1
trunk 1.0.0 1.0.0-beta.1 1.0.0-beta.2 1.0.0-beta.3 1.0.0-rc.1 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 2.1.0 2.2.0 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.7.0 2.8.0 All 44 releases
performance-lab / includes / admin / plugins.php

plugins.php in Performance Lab 3.6.1, at includes/admin/plugins.php

591 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin settings helper functions.
4 *
5 * @package performance-lab
6 * @noinspection PhpRedundantOptionalArgumentInspection
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit; // Exit if accessed directly.
11 }
12
13 /**
14 * Gets plugin info for the given plugin slug from WordPress.org.
15 *
16 * @since 2.8.0
17 *
18 * @param string $plugin_slug The string identifier for the plugin in questions slug.
19 * @return array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], download_link: string, version: string}|WP_Error Array of plugin data or WP_Error if failed.
20 */
21 function perflab_query_plugin_info( string $plugin_slug ) {
22 $transient_key = 'perflab_plugins_info';
23 $plugins = get_transient( $transient_key );
24
25 if ( is_array( $plugins ) ) {
26 // If the specific plugin_slug is not in the cache, return an error.
27 if ( ! isset( $plugins[ $plugin_slug ] ) ) {
28 return new WP_Error(
29 'plugin_not_found',
30 __( 'Plugin not found in cached API response.', 'performance-lab' )
31 );
32 }
33 return $plugins[ $plugin_slug ]; // Return cached plugin info if found.
34 }
35
36 $fields = array(
37 'name',
38 'slug',
39 'short_description',
40 'requires',
41 'requires_php',
42 'requires_plugins',
43 'download_link',
44 'version', // Needed by install_plugin_install_status().
45 );
46
47 // Proceed with API request since no cache hit.
48 $response = plugins_api(
49 'query_plugins',
50 array(
51 'author' => 'wordpressdotorg',
52 'tag' => 'performance',
53 'per_page' => 100,
54 'fields' => array_fill_keys( $fields, true ),
55 )
56 );
57
58 if ( is_wp_error( $response ) ) {
59 return new WP_Error(
60 'api_error',
61 sprintf(
62 /* translators: %s: API error message */
63 __( 'Failed to retrieve plugins data from WordPress.org API: %s', 'performance-lab' ),
64 $response->get_error_message()
65 )
66 );
67 }
68
69 // Check if the response contains plugins.
70 if ( ! ( is_object( $response ) && property_exists( $response, 'plugins' ) ) ) {
71 return new WP_Error( 'no_plugins', __( 'No plugins found in the API response.', 'performance-lab' ) );
72 }
73
74 $plugins = array();
75 $standalone_plugins = array_merge(
76 array_flip( perflab_get_standalone_plugins() ),
77 array( 'optimization-detective' => array() ) // TODO: Programmatically discover the plugin dependencies and add them here. See <https://github.com/WordPress/performance/issues/1616>.
78 );
79 foreach ( $response->plugins as $plugin_data ) {
80 if ( ! isset( $standalone_plugins[ $plugin_data['slug'] ] ) ) {
81 continue;
82 }
83 $plugins[ $plugin_data['slug'] ] = wp_array_slice_assoc( $plugin_data, $fields );
84 }
85
86 set_transient( $transient_key, $plugins, HOUR_IN_SECONDS );
87
88 if ( ! isset( $plugins[ $plugin_slug ] ) ) {
89 return new WP_Error(
90 'plugin_not_found',
91 __( 'Plugin not found in API response.', 'performance-lab' )
92 );
93 }
94
95 /**
96 * Validated (mostly) plugin data.
97 *
98 * @var array<string, array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], download_link: string, version: string}> $plugins
99 */
100 return $plugins[ $plugin_slug ];
101 }
102
103 /**
104 * Returns an array of WPP standalone plugins.
105 *
106 * @since 2.8.0
107 *
108 * @return string[] List of WPP standalone plugins as slugs.
109 */
110 function perflab_get_standalone_plugins(): array {
111 return array_keys(
112 perflab_get_standalone_plugin_data()
113 );
114 }
115
116 /**
117 * Renders plugin UI for managing standalone plugins within PL Settings screen.
118 *
119 * @since 2.8.0
120 */
121 function perflab_render_plugins_ui(): void {
122 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
123 require_once ABSPATH . 'wp-admin/includes/plugin.php';
124
125 $plugins = array();
126 $errors = array();
127
128 $standalone_plugin_data = perflab_get_standalone_plugin_data();
129 foreach ( $standalone_plugin_data as $plugin_slug => $plugin_data ) {
130 $api_data = perflab_query_plugin_info( $plugin_slug ); // Data from wordpress.org.
131
132 // Skip if the plugin is not on WordPress.org or there was a network error.
133 if ( $api_data instanceof WP_Error ) {
134 $errors[ $plugin_slug ] = $api_data;
135 } else {
136 $plugins[ $plugin_slug ] = array_merge(
137 array(
138 'experimental' => false,
139 ),
140 $plugin_data, // Data defined within Performance Lab.
141 $api_data
142 );
143 }
144 }
145
146 if ( count( $errors ) > 0 ) {
147 $active_plugins = array_map(
148 static function ( string $file ) {
149 return strtok( $file, '/' );
150 },
151 array_keys( get_plugins() )
152 );
153 $plugin_list = '<ul>';
154 $error_messages = array();
155 foreach ( $errors as $plugin_slug => $error ) {
156 if ( defined( $standalone_plugin_data[ $plugin_slug ]['constant'] ) ) {
157 $status = __( '(active)', 'performance-lab' );
158 } elseif ( in_array( $plugin_slug, $active_plugins, true ) ) {
159 $status = __( '(installed)', 'performance-lab' );
160 } else {
161 $status = '';
162 }
163
164 $plugin_list .= sprintf(
165 '<li><a target="_blank" href="%s"><code>%s</code></a> %s</li>',
166 esc_url( trailingslashit( __( 'https://wordpress.org/plugins/', 'default' ) . $plugin_slug ) ),
167 esc_html( $plugin_slug ),
168 esc_html( $status )
169 );
170 $error_messages[] = $error->get_error_message();
171 }
172 $plugin_list .= '</ul>';
173
174 $error_messages = array_unique( $error_messages );
175
176 if ( count( $error_messages ) === 1 ) {
177 $error_text = __( 'Failed to query WordPress.org Plugin Directory for the following plugin:', 'performance-lab' );
178 $error_occurred_text = __( 'The following error occurred:', 'performance-lab' );
179 } else {
180 $error_text = __( 'Failed to query WordPress.org Plugin Directory for the following plugins:', 'performance-lab' );
181 $error_occurred_text = __( 'The following errors occurred:', 'performance-lab' );
182 }
183
184 wp_admin_notice(
185 '<p>' . esc_html( $error_text ) . '</p>' .
186 $plugin_list .
187 '<p>' . esc_html( $error_occurred_text ) . '</p>' .
188 '<ul><li>' .
189 join(
190 '</li><li>',
191 array_map(
192 static function ( string $error_message ): string {
193 return wp_kses( $error_message, array( 'a' => array( 'href' => true ) ) );
194 },
195 $error_messages
196 )
197 )
198 . '</li></ul>' .
199 '<p>' . esc_html__( 'Please consider manual plugin installation and activation. You can then access each plugin\'s settings via its respective "Settings" link on the Plugins screen.', 'performance-lab' ) . '</p>',
200 array(
201 'type' => 'error',
202 'paragraph_wrap' => false,
203 )
204 );
205 }
206
207 /*
208 * Sort plugins alphabetically, with experimental ones coming last.
209 * Even though `experimental` is a boolean flag, the underlying
210 * algorithm (`usort` with `strcmp`) makes it possible to sort by it.
211 */
212 $plugins = wp_list_sort(
213 $plugins,
214 array(
215 'experimental' => 'ASC',
216 'name' => 'ASC',
217 )
218 );
219 if ( count( $plugins ) === 0 ) {
220 return;
221 }
222 ?>
223 <div class="wrap plugin-install-php">
224 <h1><?php esc_html_e( 'Performance Features', 'performance-lab' ); ?></h1>
225 <div class="wrap">
226 <form id="plugin-filter" method="post">
227 <div class="wp-list-table widefat plugin-install wpp-standalone-plugins">
228 <h2 class="screen-reader-text"><?php esc_html_e( 'Plugins list', 'default' ); ?></h2>
229 <div id="the-list">
230 <?php
231 foreach ( $plugins as $plugin_data ) {
232 perflab_render_plugin_card( $plugin_data );
233 }
234 ?>
235 </div>
236 </div>
237 </form>
238 </div>
239 <div class="clear"></div>
240 </div>
241 <?php
242 }
243
244 /**
245 * Checks if a given plugin is available.
246 *
247 * @since 3.1.0
248 * @see perflab_install_and_activate_plugin()
249 *
250 * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API.
251 * @param array<string, array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool}> $processed_plugin_availabilities Plugin availabilities already processed. This param is only used by recursive calls.
252 * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool} Availability.
253 */
254 function perflab_get_plugin_availability( array $plugin_data, array &$processed_plugin_availabilities = array() ): array {
255 if ( array_key_exists( $plugin_data['slug'], $processed_plugin_availabilities ) ) {
256 // Prevent infinite recursion by returning the previously-computed value.
257 return $processed_plugin_availabilities[ $plugin_data['slug'] ];
258 }
259
260 $availability = array(
261 'compatible_php' => (
262 false === $plugin_data['requires_php'] ||
263 is_php_version_compatible( $plugin_data['requires_php'] )
264 ),
265 'compatible_wp' => (
266 false === $plugin_data['requires'] ||
267 is_wp_version_compatible( $plugin_data['requires'] )
268 ),
269 );
270
271 $plugin_status = install_plugin_install_status( $plugin_data );
272
273 $availability['installed'] = ( 'install' !== $plugin_status['status'] );
274 $availability['activated'] = false !== $plugin_status['file'] && is_plugin_active( $plugin_status['file'] );
275
276 // The plugin is already installed or the user can install plugins.
277 $availability['can_install'] = (
278 $availability['installed'] ||
279 current_user_can( 'install_plugins' )
280 );
281
282 // The plugin is activated or the user can activate plugins.
283 $availability['can_activate'] = (
284 $availability['activated'] ||
285 false !== $plugin_status['file'] // When not false, the plugin is installed.
286 ? current_user_can( 'activate_plugin', $plugin_status['file'] )
287 : current_user_can( 'activate_plugins' )
288 );
289
290 // Store pending availability before recursing.
291 $processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability;
292
293 foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) {
294 $dependency_plugin_data = perflab_query_plugin_info( $requires_plugin );
295 if ( $dependency_plugin_data instanceof WP_Error ) {
296 continue;
297 }
298
299 $dependency_availability = perflab_get_plugin_availability( $dependency_plugin_data );
300 foreach ( array( 'compatible_php', 'compatible_wp', 'can_install', 'can_activate', 'installed', 'activated' ) as $key ) {
301 $availability[ $key ] = $availability[ $key ] && $dependency_availability[ $key ];
302 }
303 }
304
305 $processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability;
306 return $availability;
307 }
308
309 /**
310 * Installs and activates a plugin by its slug.
311 *
312 * Dependencies are recursively installed and activated as well.
313 *
314 * @since 3.1.0
315 * @see perflab_get_plugin_availability()
316 *
317 * @param string $plugin_slug Plugin slug.
318 * @param string[] $processed_plugins Slugs for plugins which have already been processed. This param is only used by recursive calls.
319 * @return WP_Error|null WP_Error on failure.
320 */
321 function perflab_install_and_activate_plugin( string $plugin_slug, array &$processed_plugins = array() ): ?WP_Error {
322 if ( in_array( $plugin_slug, $processed_plugins, true ) ) {
323 // Prevent infinite recursion from possible circular dependency.
324 return null;
325 }
326 $processed_plugins[] = $plugin_slug;
327
328 $plugin_data = perflab_query_plugin_info( $plugin_slug );
329 if ( $plugin_data instanceof WP_Error ) {
330 return $plugin_data;
331 }
332
333 // Add recommended plugins (soft dependencies) to the list of plugins installed and activated.
334 if ( 'embed-optimizer' === $plugin_slug ) {
335 $plugin_data['requires_plugins'][] = 'optimization-detective';
336 }
337
338 // Install and activate plugin dependencies first.
339 foreach ( $plugin_data['requires_plugins'] as $requires_plugin_slug ) {
340 $result = perflab_install_and_activate_plugin( $requires_plugin_slug );
341 if ( $result instanceof WP_Error ) {
342 return $result;
343 }
344 }
345
346 // Install the plugin.
347 $plugin_status = install_plugin_install_status( $plugin_data );
348 $plugin_file = $plugin_status['file'];
349 if ( 'install' === $plugin_status['status'] ) {
350 if ( ! current_user_can( 'install_plugins' ) ) {
351 return new WP_Error( 'cannot_install_plugin', __( 'Sorry, you are not allowed to install plugins on this site.', 'default' ) );
352 }
353
354 // Replace new Plugin_Installer_Skin with new Quiet_Upgrader_Skin when output needs to be suppressed.
355 $skin = new WP_Ajax_Upgrader_Skin( array( 'api' => $plugin_data ) );
356 $upgrader = new Plugin_Upgrader( $skin );
357 $result = $upgrader->install( $plugin_data['download_link'] );
358
359 if ( is_wp_error( $result ) ) {
360 return $result;
361 } elseif ( is_wp_error( $skin->result ) ) {
362 return $skin->result;
363 } elseif ( $skin->get_errors()->has_errors() ) {
364 return $skin->get_errors();
365 }
366
367 $plugins = get_plugins( '/' . $plugin_slug );
368 if ( count( $plugins ) === 0 ) {
369 return new WP_Error(
370 'plugin_not_found',
371 __( 'Plugin not found among installed plugins.', 'performance-lab' )
372 );
373 }
374
375 $plugin_file_names = array_keys( $plugins );
376 $plugin_file = $plugin_slug . '/' . $plugin_file_names[0];
377 }
378
379 // Activate the plugin.
380 if ( ! is_plugin_active( $plugin_file ) ) {
381 if ( ! current_user_can( 'activate_plugin', $plugin_file ) ) {
382 return new WP_Error( 'cannot_activate_plugin', __( 'Sorry, you are not allowed to activate this plugin.', 'default' ) );
383 }
384
385 $result = activate_plugin( $plugin_file );
386 if ( $result instanceof WP_Error ) {
387 return $result;
388 }
389 }
390
391 return null;
392 }
393
394 /**
395 * Renders individual plugin cards.
396 *
397 * This is adapted from `WP_Plugin_Install_List_Table::display_rows()` in core.
398 *
399 * @since 2.8.0
400 *
401 * @see WP_Plugin_Install_List_Table::display_rows()
402 * @link https://github.com/WordPress/wordpress-develop/blob/0b8ca16ea3bd9722bd1a38f8ab68901506b1a0e7/src/wp-admin/includes/class-wp-plugin-install-list-table.php#L467-L830
403 *
404 * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string, experimental: bool} $plugin_data Plugin data augmenting data from the WordPress.org API.
405 */
406 function perflab_render_plugin_card( array $plugin_data ): void {
407
408 $name = wp_strip_all_tags( $plugin_data['name'] );
409 $description = wp_strip_all_tags( $plugin_data['short_description'] );
410
411 /** This filter is documented in wp-admin/includes/class-wp-plugin-install-list-table.php */
412 $description = apply_filters( 'plugin_install_description', $description, $plugin_data );
413
414 $availability = perflab_get_plugin_availability( $plugin_data );
415 $compatible_php = $availability['compatible_php'];
416 $compatible_wp = $availability['compatible_wp'];
417
418 $action_links = array();
419
420 if ( $availability['activated'] ) {
421 $action_links[] = sprintf(
422 '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
423 esc_html( _x( 'Active', 'plugin', 'default' ) )
424 );
425 } elseif (
426 $availability['compatible_php'] &&
427 $availability['compatible_wp'] &&
428 $availability['can_install'] &&
429 $availability['can_activate']
430 ) {
431 $url = esc_url_raw(
432 add_query_arg(
433 array(
434 'action' => 'perflab_install_activate_plugin',
435 '_wpnonce' => wp_create_nonce( 'perflab_install_activate_plugin' ),
436 'slug' => $plugin_data['slug'],
437 ),
438 admin_url( 'options-general.php' )
439 )
440 );
441
442 $action_links[] = sprintf(
443 '<a class="button perflab-install-active-plugin" href="%s" data-plugin-slug="%s">%s</a>',
444 esc_url( $url ),
445 esc_attr( $plugin_data['slug'] ),
446 esc_html__( 'Activate', 'default' )
447 );
448 } else {
449 $explanation = $availability['can_install'] ? _x( 'Cannot Activate', 'plugin', 'default' ) : _x( 'Cannot Install', 'plugin', 'default' );
450 $action_links[] = sprintf(
451 '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
452 esc_html( $explanation )
453 );
454 }
455
456 if ( current_user_can( 'install_plugins' ) ) {
457 $title_link_attr = ' class="thickbox open-plugin-details-modal"';
458 $details_link = esc_url_raw(
459 add_query_arg(
460 array(
461 'tab' => 'plugin-information',
462 'plugin' => $plugin_data['slug'],
463 'TB_iframe' => 'true',
464 'width' => 600,
465 'height' => 550,
466 ),
467 admin_url( 'plugin-install.php' )
468 )
469 );
470
471 $action_links[] = sprintf(
472 '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
473 esc_url( $details_link ),
474 /* translators: %s: Plugin name and version. */
475 esc_attr( sprintf( __( 'More information about %s', 'default' ), $name ) ),
476 esc_attr( $name ),
477 esc_html__( 'Learn more', 'performance-lab' )
478 );
479 } else {
480 $title_link_attr = ' target="_blank"';
481
482 /* translators: %s: Plugin name. */
483 $aria_label = sprintf( __( 'Visit plugin site for %s', 'default' ), $name );
484
485 $details_link = __( 'https://wordpress.org/plugins/', 'default' ) . $plugin_data['slug'] . '/';
486
487 $action_links[] = sprintf(
488 '<a href="%s" aria-label="%s" target="_blank">%s</a>',
489 esc_url( $details_link ),
490 esc_attr( $aria_label ),
491 esc_html__( 'Visit plugin site', 'default' )
492 );
493 }
494
495 if ( $availability['activated'] ) {
496 $settings_url = perflab_get_plugin_settings_url( $plugin_data['slug'] );
497 if ( null !== $settings_url ) {
498 /* translators: %s is the settings URL */
499 $action_links[] = sprintf( '<a href="%s">%s</a>', esc_url( $settings_url ), esc_html__( 'Settings', 'performance-lab' ) );
500 }
501 }
502 ?>
503 <div class="plugin-card plugin-card-<?php echo sanitize_html_class( $plugin_data['slug'] ); ?>">
504 <?php
505 if ( ! $compatible_php || ! $compatible_wp ) {
506 echo '<div class="notice inline notice-error notice-alt">';
507 if ( ! $compatible_php && ! $compatible_wp ) {
508 echo '<p>' . esc_html_e( 'This plugin does not work with your versions of WordPress and PHP.', 'default' ) . '</p>';
509 if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
510 echo wp_kses_post(
511 sprintf(
512 /* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
513 ' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.', 'default' ),
514 esc_url( self_admin_url( 'update-core.php' ) ),
515 esc_url( wp_get_update_php_url() )
516 )
517 );
518 wp_update_php_annotation( '<p><em>', '</em></p>' );
519 } elseif ( current_user_can( 'update_core' ) ) {
520 echo wp_kses_post(
521 sprintf(
522 /* translators: %s: URL to WordPress Updates screen. */
523 ' ' . __( '<a href="%s">Please update WordPress</a>.', 'default' ),
524 esc_url( self_admin_url( 'update-core.php' ) )
525 )
526 );
527 } elseif ( current_user_can( 'update_php' ) ) {
528 echo wp_kses_post(
529 sprintf(
530 /* translators: %s: URL to Update PHP page. */
531 ' ' . __( '<a href="%s">Learn more about updating PHP</a>.', 'default' ),
532 esc_url( wp_get_update_php_url() )
533 )
534 );
535 wp_update_php_annotation( '<p><em>', '</em></p>' );
536 }
537 } elseif ( ! $compatible_wp ) {
538 esc_html_e( 'This plugin does not work with your version of WordPress.', 'default' );
539 if ( current_user_can( 'update_core' ) ) {
540 echo wp_kses_post(
541 sprintf(
542 /* translators: %s: URL to WordPress Updates screen. */
543 ' ' . __( '<a href="%s">Please update WordPress</a>.', 'default' ),
544 esc_url( self_admin_url( 'update-core.php' ) )
545 )
546 );
547 }
548 } elseif ( ! $compatible_php ) {
549 esc_html_e( 'This plugin does not work with your version of PHP.', 'default' );
550 if ( current_user_can( 'update_php' ) ) {
551 echo wp_kses_post(
552 sprintf(
553 /* translators: %s: URL to Update PHP page. */
554 ' ' . __( '<a href="%s">Learn more about updating PHP</a>.', 'default' ),
555 esc_url( wp_get_update_php_url() )
556 )
557 );
558 wp_update_php_annotation( '<p><em>', '</em></p>' );
559 }
560 }
561 echo '</div>';
562 }
563 ?>
564 <div class="plugin-card-top">
565 <div class="name column-name">
566 <h3>
567 <a href="<?php echo esc_url( $details_link ); ?>"<?php echo $title_link_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
568 <?php echo wp_kses_post( $name ); ?>
569 </a>
570 <?php if ( $plugin_data['experimental'] ) : ?>
571 <em class="perflab-plugin-experimental">
572 <?php echo esc_html( _x( '(experimental)', 'plugin suffix', 'performance-lab' ) ); ?>
573 </em>
574 <?php endif; ?>
575 </h3>
576 </div>
577 <div class="action-links">
578 <ul class="plugin-action-buttons">
579 <?php foreach ( $action_links as $action_link ) : ?>
580 <li><?php echo wp_kses_post( $action_link ); ?></li>
581 <?php endforeach; ?>
582 </ul>
583 </div>
584 <div class="desc column-description">
585 <p><?php echo wp_kses_post( $description ); ?></p>
586 </div>
587 </div>
588 </div>
589 <?php
590 }
591