diff --git a/plugins/performance-lab/includes/admin/load.php b/plugins/performance-lab/includes/admin/load.php index 819c116e28..26b9e9775e 100644 --- a/plugins/performance-lab/includes/admin/load.php +++ b/plugins/performance-lab/includes/admin/load.php @@ -49,9 +49,6 @@ function perflab_load_features_page(): void { // Handle style for settings page. add_action( 'admin_head', 'perflab_print_features_page_style' ); - - // Handle script for settings page. - add_action( 'admin_footer', 'perflab_print_plugin_progress_indicator_script' ); } /** @@ -228,8 +225,14 @@ function perflab_enqueue_features_page_scripts(): void { wp_enqueue_style( 'thickbox' ); wp_enqueue_script( 'plugin-install' ); - // Enqueue the a11y script. - wp_enqueue_script( 'wp-a11y' ); + // Enqueue plugin activate AJAX script and localize script data. + wp_enqueue_script( + 'perflab-plugin-activate-ajax', + plugin_dir_url( PERFLAB_MAIN_FILE ) . 'includes/admin/plugin-activate-ajax.js', + array( 'wp-i18n', 'wp-a11y', 'wp-api-fetch' ), + PERFLAB_VERSION, + true + ); } /** @@ -396,42 +399,6 @@ static function ( $name ) { } } -/** - * Callback function that print plugin progress indicator script. - * - * @since 3.1.0 - */ -function perflab_print_plugin_progress_indicator_script(): void { - $js_function = << 'module' ) - ); -} - /** * Gets the URL to the plugin settings screen if one exists. * diff --git a/plugins/performance-lab/includes/admin/plugin-activate-ajax.js b/plugins/performance-lab/includes/admin/plugin-activate-ajax.js new file mode 100644 index 0000000000..fd48fbb252 --- /dev/null +++ b/plugins/performance-lab/includes/admin/plugin-activate-ajax.js @@ -0,0 +1,88 @@ +/** + * Handles activation of Performance Features (Plugins) using AJAX. + */ + +( function () { + // @ts-ignore + const { i18n, a11y, apiFetch } = wp; + const { __ } = i18n; + + /** + * Handles click events on elements with the class 'perflab-install-active-plugin'. + * + * This asynchronous function listens for click events on the document and executes + * the provided callback function if triggered. + * + * @param {MouseEvent} event - The click event object that is triggered when the user clicks on the document. + * + * @return {Promise} The asynchronous function returns a promise that resolves to void. + */ + async function handlePluginActivationClick( event ) { + const target = /** @type {HTMLElement} */ ( event.target ); + + // Prevent the default link behavior. + event.preventDefault(); + + if ( + target.classList.contains( 'updating-message' ) || + target.classList.contains( 'disabled' ) + ) { + return; + } + + target.classList.add( 'updating-message' ); + target.textContent = __( 'Activating…', 'performance-lab' ); + + a11y.speak( __( 'Activating…', 'performance-lab' ) ); + + const pluginSlug = target.dataset.pluginSlug; + + try { + // Activate the plugin/feature via the REST API. + await apiFetch( { + path: `/performance-lab/v1/features/${ pluginSlug }:activate`, + method: 'POST', + } ); + + // Fetch the plugin/feature information via the REST API. + /** @type {{settingsUrl: string|null}} */ + const featureInfo = await apiFetch( { + path: `/performance-lab/v1/features/${ pluginSlug }`, + method: 'GET', + } ); + + if ( featureInfo.settingsUrl ) { + const actionButtonList = document.querySelector( + `.plugin-card-${ pluginSlug } .plugin-action-buttons` + ); + + const listItem = document.createElement( 'li' ); + const anchor = document.createElement( 'a' ); + + anchor.href = featureInfo.settingsUrl; + anchor.textContent = __( 'Settings', 'performance-lab' ); + + listItem.appendChild( anchor ); + actionButtonList.appendChild( listItem ); + } + + a11y.speak( __( 'Plugin activated.', 'performance-lab' ) ); + + target.textContent = __( 'Active', 'performance-lab' ); + target.classList.remove( 'updating-message' ); + target.classList.add( 'disabled' ); + } catch ( error ) { + a11y.speak( __( 'Plugin failed to activate.', 'performance-lab' ) ); + + target.classList.remove( 'updating-message' ); + target.textContent = __( 'Activate', 'performance-lab' ); + } + } + + // Attach the event listeners. + document + .querySelectorAll( '.perflab-install-active-plugin' ) + .forEach( ( item ) => { + item.addEventListener( 'click', handlePluginActivationClick ); + } ); +} )(); diff --git a/plugins/performance-lab/includes/admin/plugins.php b/plugins/performance-lab/includes/admin/plugins.php index 5aeba3b6f0..1e91c66cf2 100644 --- a/plugins/performance-lab/includes/admin/plugins.php +++ b/plugins/performance-lab/includes/admin/plugins.php @@ -440,8 +440,9 @@ function perflab_render_plugin_card( array $plugin_data ): void { ); $action_links[] = sprintf( - '%s', + '%s', esc_url( $url ), + esc_attr( $plugin_data['slug'] ), esc_html__( 'Activate', 'default' ) ); } else { diff --git a/plugins/performance-lab/includes/admin/rest-api.php b/plugins/performance-lab/includes/admin/rest-api.php new file mode 100644 index 0000000000..43da98987e --- /dev/null +++ b/plugins/performance-lab/includes/admin/rest-api.php @@ -0,0 +1,183 @@ +[a-z0-9_-]+):activate'; + +/** + * Route for fetching plugin/feature information. + * + * @since n.e.x.t + * @var string + */ +const PERFLAB_FEATURES_INFORMATION_ROUTE = '/features/(?P[a-z0-9_-]+)'; + +/** + * Registers endpoint for performance-lab REST API. + * + * @since n.e.x.t + * @access private + */ +function perflab_register_endpoint(): void { + register_rest_route( + PERFLAB_REST_API_NAMESPACE, + PERFLAB_FEATURES_ACTIVATE_ROUTE, + array( + 'methods' => 'POST', + 'args' => array( + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Plugin slug of the Performance Lab feature to be activated.', 'performance-lab' ), + 'required' => true, + 'validate_callback' => 'perflab_validate_slug_endpoint_arg', + ), + ), + 'callback' => 'perflab_handle_feature_activation', + 'permission_callback' => static function () { + // Important: The endpoint calls perflab_install_and_activate_plugin() which does more granular capability checks. + if ( current_user_can( 'activate_plugins' ) ) { + return true; + } + + return new WP_Error( 'cannot_activate', __( 'Sorry, you are not allowed to activate this feature.', 'performance-lab' ) ); + }, + ) + ); + + register_rest_route( + PERFLAB_REST_API_NAMESPACE, + PERFLAB_FEATURES_INFORMATION_ROUTE, + array( + 'methods' => 'GET', + 'args' => array( + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Plugin slug of plugin/feature whose information is needed.', 'performance-lab' ), + 'required' => true, + 'validate_callback' => 'perflab_validate_slug_endpoint_arg', + ), + ), + 'callback' => 'perflab_handle_get_feature_information', + 'permission_callback' => static function () { + if ( current_user_can( 'manage_options' ) ) { + return true; + } + + return new WP_Error( 'cannot_access_plugin_settings_url', __( 'Sorry, you are not allowed to access plugin/feature information on this site.', 'performance-lab' ) ); + }, + ) + ); +} +add_action( 'rest_api_init', 'perflab_register_endpoint' ); + +/** + * Validates whether the provided plugin slug is a valid Performance Lab plugin. + * + * Note that an enum is not being used because additional PHP files have to be required to access the necessary functions, + * and this would not be ideal to do at rest_api_init. + * + * @since n.e.x.t + * @access private + * + * @param string $slug Plugin slug. + * @return bool Whether valid. + */ +function perflab_validate_slug_endpoint_arg( string $slug ): bool { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php'; + require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php'; + return in_array( $slug, perflab_get_standalone_plugins(), true ); +} + +/** + * Handles REST API request to activate plugin/feature. + * + * @since n.e.x.t + * @access private + * + * @phpstan-param WP_REST_Request> $request + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response|WP_Error Response. + */ +function perflab_handle_feature_activation( WP_REST_Request $request ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php'; + + // Install and activate the plugin/feature and its dependencies. + $result = perflab_install_and_activate_plugin( $request['slug'] ); + if ( is_wp_error( $result ) ) { + switch ( $result->get_error_code() ) { + case 'cannot_install_plugin': + case 'cannot_activate_plugin': + $response_code = rest_authorization_required_code(); + break; + case 'plugin_not_found': + $response_code = 404; + break; + default: + $response_code = 500; + } + return new WP_Error( + $result->get_error_code(), + $result->get_error_message(), + array( 'status' => $response_code ) + ); + } + + return new WP_REST_Response( + array( + 'success' => true, + ) + ); +} + +/** + * Handles REST API request to get plugin/feature information. + * + * @since n.e.x.t + * @access private + * + * @phpstan-param WP_REST_Request> $request + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response Response. + */ +function perflab_handle_get_feature_information( WP_REST_Request $request ): WP_REST_Response { + $plugin_settings_url = perflab_get_plugin_settings_url( $request['slug'] ); + + return new WP_REST_Response( + array( + 'slug' => $request['slug'], + 'settingsUrl' => $plugin_settings_url, + ) + ); +} diff --git a/plugins/performance-lab/load.php b/plugins/performance-lab/load.php index 304089d13c..02ee034cae 100644 --- a/plugins/performance-lab/load.php +++ b/plugins/performance-lab/load.php @@ -339,3 +339,6 @@ function perflab_cleanup_option(): void { require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/server-timing.php'; require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php'; } + +// Load REST API. +require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/rest-api.php';