Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use AJAX for activating features / plugins in Performance Lab #1646

Merged
merged 25 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47f3938
Update print plugin progress indicator script function to support aja…
b1ink0 Nov 7, 2024
9d97f59
Add admin notice dynamically, Update to use async/await
b1ink0 Nov 8, 2024
6f172ad
Add settings URL of plugin in admin notice
b1ink0 Nov 11, 2024
337da6c
Move inline JavaScript to separate file
b1ink0 Nov 11, 2024
58c37d9
Refactor JavaScript code to use jQuery
b1ink0 Nov 11, 2024
a59909d
Update to use IIFE, Remove unnecessary doc comment
b1ink0 Nov 11, 2024
f9faaea
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 11, 2024
770d462
Merge branch 'WordPress:trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 12, 2024
e6710a3
Remove admin notice part, Remove constant, Use plugin_url for JS file…
b1ink0 Nov 12, 2024
c3d3588
Refactor to use vanilla JavaScript
b1ink0 Nov 12, 2024
fdf7e0b
Fix duplicate request on multiple click, Fix settings URL not showing…
b1ink0 Nov 12, 2024
b3ecc98
Remove wp-a11y enqueue, Revert changes to Speculative Loading plugin …
b1ink0 Nov 13, 2024
c713e47
Add REST API endpoints for activating plugin and getting plugin setti…
b1ink0 Nov 13, 2024
1ebe323
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 13, 2024
c134fe2
Update REST API routes, Add validation to args of REST API request, R…
b1ink0 Nov 14, 2024
369487f
Update REST API routes, Update JavaScript code to use updated REST AP…
b1ink0 Nov 15, 2024
d84e9bd
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 15, 2024
9bee27e
Fix inline comments and error messages
b1ink0 Nov 15, 2024
ce217ba
Add missing since, Remove unnecessary check in JavaScript
b1ink0 Nov 15, 2024
fa5d869
Add typing for featureInfo
westonruter Nov 15, 2024
3a333ae
Clarify description of slug arg
westonruter Nov 15, 2024
6250658
Use `plugin_dir_url()` for now.
felixarntz Nov 15, 2024
a8d89a8
Fix capability check on installation/activation endpoint.
felixarntz Nov 15, 2024
c593ae6
Use more appropriate HTTP response codes for different errors.
felixarntz Nov 15, 2024
e0691bf
Add missing require_once to prevent fatal error when installing plugi…
felixarntz Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 8 additions & 41 deletions plugins/performance-lab/includes/admin/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
}

/**
Expand Down Expand Up @@ -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',
plugins_url( 'includes/admin/plugin-activate-ajax.js', PERFLAB_MAIN_FILE ),
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
array( 'wp-i18n', 'wp-a11y', 'wp-api-fetch' ),
PERFLAB_VERSION,
true
);
}

/**
Expand Down Expand Up @@ -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 = <<<JS
function addPluginProgressIndicator( message ) {
document.addEventListener( 'DOMContentLoaded', function () {
document.addEventListener( 'click', function ( event ) {
if (
event.target.classList.contains(
'perflab-install-active-plugin'
)
) {
const target = event.target;
target.classList.add( 'updating-message' );
target.textContent = message;

wp.a11y.speak( message );
}
} );
} );
}
JS;

wp_print_inline_script_tag(
sprintf(
'( %s )( %s );',
$js_function,
wp_json_encode( __( 'Activating...', 'default' ) )
),
array( 'type' => 'module' )
);
}

/**
* Gets the URL to the plugin settings screen if one exists.
*
Expand Down
87 changes: 87 additions & 0 deletions plugins/performance-lab/includes/admin/plugin-activate-ajax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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<void>} 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();

westonruter marked this conversation as resolved.
Show resolved Hide resolved
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.
const featureInfo = await apiFetch( {
path: `/performance-lab/v1/features/${ pluginSlug }`,
method: 'GET',
} );

if ( featureInfo?.settingsUrl ) {
westonruter marked this conversation as resolved.
Show resolved Hide resolved
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' );
}
}

westonruter marked this conversation as resolved.
Show resolved Hide resolved
// Attach the event listeners.
document
.querySelectorAll( '.perflab-install-active-plugin' )
.forEach( ( item ) => {
item.addEventListener( 'click', handlePluginActivationClick );
} );
} )();
3 changes: 2 additions & 1 deletion plugins/performance-lab/includes/admin/plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,9 @@ function perflab_render_plugin_card( array $plugin_data ): void {
);

$action_links[] = sprintf(
'<a class="button perflab-install-active-plugin" href="%s">%s</a>',
'<a class="button perflab-install-active-plugin" href="%s" data-plugin-slug="%s">%s</a>',
esc_url( $url ),
esc_attr( $plugin_data['slug'] ),
esc_html__( 'Activate', 'default' )
);
} else {
Expand Down
167 changes: 167 additions & 0 deletions plugins/performance-lab/includes/admin/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
/**
* REST API integration for the plugin.
*
* @package performance-lab
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Namespace for performance-lab REST API.
*
westonruter marked this conversation as resolved.
Show resolved Hide resolved
* @var string
*/
const PERFLAB_REST_API_NAMESPACE = 'performance-lab/v1';

/**
* Route for activating plugin/feature.
*
* Note the `:activate` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way
* that does not strictly follow the standard usage.
*
westonruter marked this conversation as resolved.
Show resolved Hide resolved
* @link https://google.aip.dev/136
* @var string
*/
const PERFLAB_FEATURES_ACTIVATE_ROUTE = '/features/(?P<slug>[a-z0-9_-]+):activate';

/**
* Route for fetching plugin/feature information.
*
westonruter marked this conversation as resolved.
Show resolved Hide resolved
* @var string
*/
const PERFLAB_FEATURES_INFORMATION_ROUTE = '/features/(?P<slug>[a-z0-9_-]+)';

westonruter marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 plugin/feature that needs to be activated.', 'performance-lab' ),
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'required' => true,
'validate_callback' => 'perflab_validate_slug_endpoint_arg',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this validates against the list of PL features/plugins, should we include an enum entry here with the possible values?

If that feels like too much, I think we should at least mention it in the description, e.g. "Must be one of the Performance Lab feature slugs."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially proposed using an enum here, but it won't work unless we then also always require the additional admin-only PHP files used in PerfLab. So what is implemented with this validate_callback is a sort of just-in-time enum. This is already explained in the phpdoc for perflab_validate_slug_endpoint_arg().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG. In that case let's just mention it in the description.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

),
),
'callback' => 'perflab_handle_feature_activation',
'permission_callback' => static function () {
if ( current_user_can( 'install_plugins' ) ) {
return true;
}

return new WP_Error( 'cannot_install_plugin', __( 'Sorry, you are not allowed to install plugins on this site.', 'performance-lab' ) );
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
},
)
);

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',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
),
),
'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<array<string, mixed>> $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/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'] );
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
if ( is_wp_error( $result ) ) {
return new WP_Error(
'plugin_activation_failed',
$result->get_error_message(),
array( 'status' => 500 )
);
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
}

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<array<string, mixed>> $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,
)
);
}
3 changes: 3 additions & 0 deletions plugins/performance-lab/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading