PluginProbe
Performance Lab / trunk
Performance Lab vtrunk
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 / load.php

load.php in Performance Lab trunk, at includes/admin/load.php

642 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin integration file.
4 *
5 * @package performance-lab
6 */
7
8 declare( strict_types = 1 );
9
10 // @codeCoverageIgnoreStart
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14 // @codeCoverageIgnoreEnd
15
16 /**
17 * Adds the features page to the Settings menu.
18 *
19 * @since 1.0.0
20 * @since 3.0.0 Renamed to perflab_add_features_page().
21 */
22 function perflab_add_features_page(): void {
23 $hook_suffix = add_options_page(
24 __( 'Performance Features', 'performance-lab' ),
25 __( 'Performance', 'performance-lab' ),
26 'manage_options',
27 PERFLAB_SCREEN,
28 'perflab_render_settings_page'
29 );
30
31 // Add the following hooks only if the screen was successfully added.
32 if ( false !== $hook_suffix ) {
33 add_action( "load-{$hook_suffix}", 'perflab_load_features_page', 10, 0 );
34 add_filter( 'plugin_action_links_' . plugin_basename( PERFLAB_MAIN_FILE ), 'perflab_plugin_action_links_add_settings' );
35 }
36 }
37
38 add_action( 'admin_menu', 'perflab_add_features_page' );
39
40 /**
41 * Initializes functionality for the features page.
42 *
43 * @since 1.0.0
44 * @since 3.0.0 Renamed to perflab_load_features_page(), and the
45 * $module and $hook_suffix parameters were removed.
46 */
47 function perflab_load_features_page(): void {
48 // Handle script enqueuing for settings page.
49 add_action( 'admin_enqueue_scripts', 'perflab_enqueue_features_page_scripts' );
50
51 // Handle admin notices for settings page.
52 add_action( 'admin_notices', 'perflab_plugin_admin_notices' );
53
54 // Handle style for settings page.
55 add_action( 'admin_head', 'perflab_print_features_page_style' );
56 }
57
58 /**
59 * Renders the plugin page.
60 *
61 * @since 1.0.0
62 * @since 3.0.0 Renamed to perflab_render_settings_page().
63 */
64 function perflab_render_settings_page(): void {
65 ?>
66 <div class="wrap">
67 <?php perflab_render_plugins_ui(); ?>
68 </div>
69 <?php
70 }
71
72 /**
73 * Gets dismissed admin pointer IDs.
74 *
75 * @since 4.0.0
76 *
77 * @return non-empty-string[] Dismissed admin pointer IDs.
78 */
79 function perflab_get_dismissed_admin_pointer_ids(): array {
80 return array_filter(
81 explode(
82 ',',
83 (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true )
84 )
85 );
86 }
87
88 /**
89 * Gets the admin pointers.
90 *
91 * @since 4.0.0
92 *
93 * @return array<non-empty-string, array{ content: string, plugin: non-empty-string, dismiss_if_installed: bool }> Keys are the admin pointer IDs.
94 */
95 function perflab_get_admin_pointers(): array {
96 $pointers = array(
97 'perflab-admin-pointer' => array(
98 'content' => __( 'You can now test upcoming WordPress performance features.', 'performance-lab' ),
99 'plugin' => 'performance-lab',
100 'dismiss_if_installed' => false,
101 ),
102 'perflab-feature-view-transitions' => array(
103 'content' => __( 'New <strong>View Transitions</strong> feature now available.', 'performance-lab' ),
104 'plugin' => 'view-transitions',
105 'dismiss_if_installed' => true,
106 ),
107 'perflab-feature-nocache-bfcache' => array(
108 'content' => __( 'New <strong>Instant Back/Forward</strong> feature now available.', 'performance-lab' ),
109 'plugin' => 'nocache-bfcache',
110 'dismiss_if_installed' => true,
111 ),
112 );
113
114 $installed_plugins = get_plugins();
115 if (
116 isset( $installed_plugins['speculation-rules/load.php']['Version'] )
117 &&
118 version_compare( $installed_plugins['speculation-rules/load.php']['Version'], '1.6.0', '>=' )
119 ) {
120 $pointers['perflab-feature-speculation-rules-auth'] = array(
121 'content' => __( '<strong>Speculative Loading</strong> now includes an opt-in setting for logged-in users.', 'performance-lab' ),
122 'plugin' => 'speculative-loading',
123 'dismiss_if_installed' => false,
124 );
125 }
126
127 return $pointers;
128 }
129
130 /**
131 * Initializes admin pointer.
132 *
133 * Handles the bootstrapping of the admin pointer.
134 * Mainly jQuery code that is self-initialising.
135 *
136 * @since 1.0.0
137 *
138 * @param string|null $hook_suffix The current admin page. Note this can be null because `iframe_header()` does not
139 * ensure that `$hook_suffix` is a string when it calls `do_action( 'admin_enqueue_scripts', $hook_suffix )`.
140 */
141 function perflab_admin_pointer( ?string $hook_suffix = '' ): void {
142 // See get_plugin_page_hookname().
143 $is_performance_screen = 'settings_page_' . PERFLAB_SCREEN === $hook_suffix;
144
145 // Do not show admin pointer in multisite Network admin, User admin UI, dashboard, or plugins list table. However,
146 // do proceed on the Performance screen so that all pointers can be auto-dismissed.
147 if (
148 is_network_admin() ||
149 is_user_admin() ||
150 (
151 ! in_array( $hook_suffix, array( 'index.php', 'plugins.php' ), true ) &&
152 ! $is_performance_screen
153 )
154 ) {
155 return;
156 }
157
158 $admin_pointers = perflab_get_admin_pointers();
159 $admin_pointer_ids = array_keys( $admin_pointers );
160 $dismissed_pointer_ids = perflab_get_dismissed_admin_pointer_ids();
161
162 // And if we're on the Performance screen, automatically dismiss all the pointers.
163 $auto_dismissed_pointer_ids = array();
164 if ( $is_performance_screen ) {
165 $auto_dismissed_pointer_ids = array_merge( $auto_dismissed_pointer_ids, $admin_pointer_ids );
166 }
167
168 // List of pointer IDs that are tied to feature plugin slugs.
169 $plugin_pointers_dismissed_if_installed = array();
170 foreach ( $admin_pointers as $pointer_id => $admin_pointer ) {
171 if ( $admin_pointer['dismiss_if_installed'] ) {
172 $plugin_pointers_dismissed_if_installed[ $pointer_id ] = $admin_pointer['plugin'];
173 }
174 }
175
176 // Preemptively dismiss plugin-specific pointers for plugins which are already installed.
177 $plugin_dependent_pointers_undismissed = array_diff( array_keys( $plugin_pointers_dismissed_if_installed ), $dismissed_pointer_ids );
178 if ( count( $plugin_dependent_pointers_undismissed ) > 0 ) {
179 /**
180 * Installed plugin slugs.
181 *
182 * @var non-empty-string[] $installed_plugin_slugs
183 */
184 $installed_plugin_slugs = array_map(
185 static function ( $name ) {
186 return strtok( $name, '/' );
187 },
188 array_keys( get_plugins() )
189 );
190
191 foreach ( $plugin_dependent_pointers_undismissed as $pointer_id ) {
192 if (
193 in_array( $plugin_pointers_dismissed_if_installed[ $pointer_id ], $installed_plugin_slugs, true ) &&
194 ! in_array( $pointer_id, $dismissed_pointer_ids, true )
195 ) {
196 $auto_dismissed_pointer_ids[] = $pointer_id;
197 }
198 }
199 }
200
201 // Persist the automatically-dismissed pointers.
202 if ( count( $auto_dismissed_pointer_ids ) > 0 ) {
203 $dismissed_pointer_ids = array_unique( array_merge( $dismissed_pointer_ids, $auto_dismissed_pointer_ids ) );
204 update_user_meta(
205 get_current_user_id(),
206 'dismissed_wp_pointers',
207 implode( ',', $dismissed_pointer_ids )
208 );
209 }
210
211 // Determine which admin pointers we need.
212 $new_install_pointer_id = 'perflab-admin-pointer';
213 if ( ! in_array( $new_install_pointer_id, $dismissed_pointer_ids, true ) ) {
214 $needed_pointer_ids = array( $new_install_pointer_id );
215 } else {
216 $needed_pointer_ids = $admin_pointer_ids;
217 }
218 $needed_pointer_ids = array_diff( $needed_pointer_ids, $dismissed_pointer_ids );
219
220 // No admin pointers are needed, so abort.
221 if ( count( $needed_pointer_ids ) === 0 ) {
222 return;
223 }
224
225 // Enqueue pointer CSS and JS.
226 wp_enqueue_style( 'wp-pointer' );
227 wp_enqueue_script( 'wp-pointer' );
228
229 $args = array(
230 'heading' => __( 'Performance Lab', 'performance-lab' ),
231 );
232
233 $args['content'] = implode(
234 '',
235 array_map(
236 static function ( string $needed_pointer ) use ( $admin_pointers ): string {
237 return '<p>' . $admin_pointers[ $needed_pointer ]['content'] . '</p>';
238 },
239 $needed_pointer_ids
240 )
241 );
242
243 $args['content'] .= '<p>' . sprintf(
244 /* translators: %s: settings page link */
245 esc_html__( 'Open %s to individually toggle the performance features and access any relevant settings.', 'performance-lab' ),
246 '<a href="' . esc_url( add_query_arg( 'page', PERFLAB_SCREEN, admin_url( 'options-general.php' ) ) ) . '">' . esc_html__( 'Settings > Performance', 'performance-lab' ) . '</a>'
247 ) . '</p>';
248
249 $wp_kses_options = array(
250 'a' => array(
251 'href' => array(),
252 ),
253 'p' => array(),
254 'strong' => array(),
255 );
256
257 $pointer_ids_to_dismiss = array_values( array_diff( $admin_pointer_ids, $dismissed_pointer_ids ) );
258
259 ob_start();
260 ?>
261 <script>
262 jQuery( function() {
263 const pointerIdsToDismiss = <?php echo wp_json_encode( $pointer_ids_to_dismiss, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_OBJECT_AS_ARRAY ); ?>;
264 const nonce = <?php echo wp_json_encode( wp_create_nonce( 'dismiss_pointer' ), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>;
265
266 function dismissNextPointer() {
267 const pointerId = pointerIdsToDismiss.shift();
268 if ( ! pointerId ) {
269 return;
270 }
271
272 jQuery.post(
273 window.ajaxurl,
274 {
275 pointer: pointerId,
276 action: 'dismiss-wp-pointer',
277 _wpnonce: nonce,
278 }
279 ).then( dismissNextPointer );
280 }
281
282 // Pointer Options.
283 const options = {
284 content: <?php echo wp_json_encode( '<h3>' . esc_html( $args['heading'] ) . '</h3>' . wp_kses( $args['content'], $wp_kses_options ), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>,
285 position: {
286 edge: 'left',
287 align: 'right',
288 },
289 pointerClass: 'wp-pointer arrow-top',
290 pointerWidth: 420,
291 close: dismissNextPointer
292 };
293
294 jQuery( '#menu-settings' ).pointer( options ).pointer( 'open' );
295 } );
296 </script>
297 <?php
298 $processor = new WP_HTML_Tag_Processor( (string) ob_get_clean() );
299 if ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) {
300 wp_add_inline_script( 'wp-pointer', $processor->get_modifiable_text() );
301 }
302 }
303 add_action( 'admin_enqueue_scripts', 'perflab_admin_pointer' );
304
305 /**
306 * Adds a link to the features page to the plugin's entry in the plugins list table.
307 *
308 * This function is only used if the features page exists and is accessible.
309 *
310 * @since 1.0.0
311 *
312 * @see perflab_add_features_page()
313 *
314 * @param string[]|mixed $links List of plugin action links HTML.
315 * @return string[]|mixed Modified list of plugin action links HTML.
316 */
317 function perflab_plugin_action_links_add_settings( $links ) {
318 if ( ! is_array( $links ) ) {
319 return $links;
320 }
321
322 // Add link as the first plugin action link.
323 $settings_link = sprintf(
324 '<a href="%s">%s</a>',
325 esc_url( add_query_arg( 'page', PERFLAB_SCREEN, admin_url( 'options-general.php' ) ) ),
326 esc_html__( 'Settings', 'performance-lab' )
327 );
328
329 return array_merge(
330 array( 'settings' => $settings_link ),
331 $links
332 );
333 }
334
335 /**
336 * Dismisses notification pointer after verifying nonce.
337 *
338 * This function adds a nonce check before dismissing perflab-admin-pointer
339 * It runs before the dismiss-wp-pointer AJAX action is performed.
340 *
341 * @since 2.3.0
342 */
343 function perflab_dismiss_wp_pointer_wrapper(): void {
344 if (
345 isset( $_POST['pointer'] )
346 &&
347 ! in_array( $_POST['pointer'], array_keys( perflab_get_admin_pointers() ), true )
348 ) {
349 // Another plugin's pointer, do nothing.
350 return;
351 }
352 check_ajax_referer( 'dismiss_pointer' );
353 }
354 add_action( 'wp_ajax_dismiss-wp-pointer', 'perflab_dismiss_wp_pointer_wrapper', 0 );
355
356 /**
357 * Gets the path to a script or stylesheet.
358 *
359 * @since 3.7.0
360 *
361 * @param string $src_path Source path.
362 * @param string|null $min_path Minified path. If not supplied, then '.min' is injected before the file extension in the source path.
363 * @return string URL to script or stylesheet.
364 */
365 function perflab_get_asset_path( string $src_path, ?string $min_path = null ): string {
366 if ( null === $min_path ) {
367 // Note: wp_scripts_get_suffix() is not used here because we need access to both the source and minified paths.
368 $min_path = (string) preg_replace( '/(?=\.\w+$)/', '.min', $src_path );
369 }
370
371 $force_src = false;
372 if ( WP_DEBUG && ! file_exists( trailingslashit( PERFLAB_PLUGIN_DIR_PATH ) . $min_path ) ) {
373 $force_src = true;
374 wp_trigger_error(
375 __FUNCTION__,
376 sprintf(
377 /* translators: %s is the minified asset path */
378 __( 'Minified asset has not been built: %s', 'performance-lab' ),
379 $min_path
380 ),
381 E_USER_WARNING
382 );
383 }
384
385 if ( SCRIPT_DEBUG || $force_src ) {
386 return $src_path;
387 }
388
389 return $min_path;
390 }
391
392 /**
393 * Callback function to handle admin scripts.
394 *
395 * @since 2.8.0
396 * @since 3.0.0 Renamed to perflab_enqueue_features_page_scripts().
397 */
398 function perflab_enqueue_features_page_scripts(): void {
399 // These assets are needed for the "Learn more" popover.
400 wp_enqueue_script( 'thickbox' );
401 wp_enqueue_style( 'thickbox' );
402 wp_enqueue_script( 'plugin-install' );
403
404 // Enqueue plugin activate AJAX script and localize script data.
405 wp_enqueue_script(
406 'perflab-plugin-activate-ajax',
407 plugins_url( perflab_get_asset_path( 'includes/admin/plugin-activate-ajax.js' ), PERFLAB_MAIN_FILE ),
408 array( 'wp-i18n', 'wp-a11y', 'wp-api-fetch' ),
409 PERFLAB_VERSION,
410 true
411 );
412 }
413
414 /**
415 * Sanitizes a plugin slug.
416 *
417 * @since 3.1.0
418 *
419 * @param mixed $unsanitized_plugin_slug Unsanitized plugin slug.
420 * @return string|null Validated and sanitized slug or else null.
421 */
422 function perflab_sanitize_plugin_slug( $unsanitized_plugin_slug ): ?string {
423 if ( in_array( $unsanitized_plugin_slug, perflab_get_standalone_plugins(), true ) ) {
424 return $unsanitized_plugin_slug;
425 }
426 return null;
427 }
428
429 /**
430 * Callback for handling installation/activation of plugin.
431 *
432 * @since 3.0.0
433 */
434 function perflab_install_activate_plugin_callback(): void {
435 check_admin_referer( 'perflab_install_activate_plugin' );
436
437 require_once ABSPATH . 'wp-admin/includes/plugin.php';
438 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
439 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
440 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
441
442 if ( ! isset( $_GET['slug'] ) ) {
443 wp_die( esc_html__( 'Missing required parameter.', 'performance-lab' ) );
444 }
445
446 $plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $_GET['slug'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- perflab_sanitize_plugin_slug() is a sanitizing function.
447 if ( null === $plugin_slug ) {
448 wp_die( esc_html__( 'Invalid plugin.', 'performance-lab' ) );
449 }
450
451 // Install and activate the plugin and its dependencies.
452 $result = perflab_install_and_activate_plugin( $plugin_slug );
453 if ( $result instanceof WP_Error ) {
454 wp_die( wp_kses_post( $result->get_error_message() ) );
455 }
456
457 $url = add_query_arg(
458 array(
459 'page' => PERFLAB_SCREEN,
460 'activate' => $plugin_slug,
461 ),
462 admin_url( 'options-general.php' )
463 );
464
465 if ( wp_safe_redirect( $url ) ) {
466 exit;
467 }
468 }
469 add_action( 'admin_action_perflab_install_activate_plugin', 'perflab_install_activate_plugin_callback' );
470
471 /**
472 * Callback function to handle admin inline style.
473 *
474 * @since 3.0.0
475 */
476 function perflab_print_features_page_style(): void {
477 ?>
478 <style>
479 .plugin-card .name,
480 .plugin-card .desc, /* For WP <6.5 versions */
481 .plugin-card .desc > p {
482 margin-left: 0;
483 }
484 .plugin-card-top {
485 /* This is required to ensure the Settings link does not extend below the bottom of a plugin card on a wide screen. */
486 min-height: 90px;
487 }
488 @media screen and (max-width: 782px) {
489 .plugin-card-top {
490 /* Same reason as above, but now the button is taller to make it easier to tap on touch screens. */
491 min-height: 110px;
492 }
493 }
494 .plugin-card .perflab-plugin-experimental {
495 font-size: 80%;
496 font-weight: normal;
497 }
498
499 @media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
500 .plugin-card .action-links {
501 margin-left: auto;
502 }
503 /* Make sure the settings link gets spaced out from the Learn more link. */
504 .plugin-card .plugin-action-buttons > li:nth-child(3) {
505 margin-left: 2ex;
506 border-left: solid 1px;
507 padding-left: 2ex;
508 }
509 }
510 </style>
511 <?php
512 }
513
514 /**
515 * Callback function hooked to admin_notices to render admin notices on the plugin's screen.
516 *
517 * @since 2.8.0
518 */
519 function perflab_plugin_admin_notices(): void {
520 if ( ! current_user_can( 'install_plugins' ) ) {
521 $are_all_plugins_installed = true;
522 $installed_plugin_slugs = array_map(
523 static function ( $name ) {
524 return strtok( $name, '/' );
525 },
526 array_keys( get_plugins() )
527 );
528 foreach ( perflab_get_standalone_plugin_version_constants() as $plugin_slug => $constant_name ) {
529 if ( ! in_array( $plugin_slug, $installed_plugin_slugs, true ) ) {
530 $are_all_plugins_installed = false;
531 break;
532 }
533 }
534
535 if ( ! $are_all_plugins_installed ) {
536 wp_admin_notice(
537 esc_html__( 'Due to your site\'s configuration, you may not be able to activate the performance features, unless the underlying plugin is already installed. Please install the relevant plugins manually.', 'performance-lab' ),
538 array(
539 'type' => 'warning',
540 )
541 );
542 return;
543 }
544 }
545
546 $activated_plugin_slug = null;
547 if ( isset( $_GET['activate'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
548 $activated_plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $_GET['activate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- perflab_sanitize_plugin_slug() is a sanitizing function.
549 }
550
551 if ( null !== $activated_plugin_slug ) {
552 $message = __( 'Feature activated.', 'performance-lab' );
553
554 $plugin_settings_url = perflab_get_plugin_settings_url( $activated_plugin_slug );
555 if ( null !== $plugin_settings_url ) {
556 /* translators: %s is the settings URL */
557 $message .= ' ' . sprintf( __( 'Review <a href="%s">settings</a>.', 'performance-lab' ), esc_url( $plugin_settings_url ) );
558 }
559
560 wp_admin_notice(
561 wp_kses(
562 $message,
563 array(
564 'a' => array(
565 'href' => array(),
566 ),
567 )
568 ),
569 array(
570 'type' => 'success',
571 'dismissible' => true,
572 )
573 );
574 }
575 }
576
577 /**
578 * Gets the URL to the plugin settings screen if one exists.
579 *
580 * @since 3.1.0
581 *
582 * @param string $plugin_slug Plugin slug passed to generate the settings link.
583 * @return string|null Either the plugin settings URL or null if not available.
584 */
585 function perflab_get_plugin_settings_url( string $plugin_slug ): ?string {
586 $plugin_file = null;
587
588 foreach ( array_keys( get_plugins() ) as $file ) {
589 if ( strtok( $file, '/' ) === $plugin_slug ) {
590 $plugin_file = $file;
591 break;
592 }
593 }
594
595 if ( null === $plugin_file ) {
596 return null;
597 }
598
599 /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
600 $plugin_links = apply_filters( "plugin_action_links_{$plugin_file}", array() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally applying core filter.
601
602 if ( ! is_array( $plugin_links ) || ! array_key_exists( 'settings', $plugin_links ) ) {
603 return null;
604 }
605
606 $p = new WP_HTML_Tag_Processor( $plugin_links['settings'] );
607 if ( ! $p->next_tag( array( 'tag_name' => 'A' ) ) ) {
608 return null;
609 }
610 $href = $p->get_attribute( 'href' );
611 if ( is_string( $href ) && '' !== $href ) {
612 return $href;
613 }
614
615 return null;
616 }
617
618 /**
619 * Prints the Performance Lab install notice after each feature plugin's row meta.
620 *
621 * @since 3.2.0
622 *
623 * @param string $plugin_file Plugin file.
624 */
625 function perflab_print_row_meta_install_notice( string $plugin_file ): void {
626 if ( ! in_array( strtok( $plugin_file, '/' ), perflab_get_standalone_plugins(), true ) ) {
627 return;
628 }
629
630 $message = sprintf(
631 /* translators: %s: link to Performance Lab settings screen */
632 __( 'This plugin is installed by <a href="%s">Performance Lab</a>.', 'performance-lab' ),
633 esc_url( add_query_arg( 'page', PERFLAB_SCREEN, admin_url( 'options-general.php' ) ) )
634 );
635
636 printf(
637 '<div class="requires"><p>%1$s</p></div>',
638 wp_kses( $message, array( 'a' => array( 'href' => array() ) ) )
639 );
640 }
641 add_action( 'after_plugin_row_meta', 'perflab_print_row_meta_install_notice' );
642