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 14 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
89 changes: 89 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,89 @@
/**
* 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.
westonruter marked this conversation as resolved.
Show resolved Hide resolved
*/
async function handlePluginActivationClick( event ) {
const target = /** @type {HTMLElement} */ ( event.target );

if ( ! target.classList.contains( 'perflab-install-active-plugin' ) ) {
return;
}

// 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 via the REST API.
await apiFetch( {
path: '/performance-lab/v1/activate-plugin',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
method: 'POST',
data: { slug: pluginSlug },
} );

// Fetch the plugin settings URL via the REST API.
const settingsResponse = await apiFetch( {
path: '/performance-lab/v1/plugin-settings-url',
method: 'POST',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
data: { slug: pluginSlug },
} );

a11y.speak( __( 'Plugin activated.', 'performance-lab' ) );

target.textContent = __( 'Active', 'performance-lab' );
target.classList.remove( 'updating-message' );
target.classList.add( 'disabled' );

const actionButtonList = document.querySelector(
`.plugin-card-${ pluginSlug } .plugin-action-buttons`
);

if ( settingsResponse?.pluginSettingsURL && actionButtonList ) {
const listItem = document.createElement( 'li' );
const anchor = document.createElement( 'a' );

anchor.href = settingsResponse?.pluginSettingsURL;
anchor.textContent = __( 'Settings', 'performance-lab' );

listItem.appendChild( anchor );
actionButtonList.appendChild( listItem );
}
westonruter marked this conversation as resolved.
Show resolved Hide resolved
} 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 listener.
document.addEventListener( 'click', handlePluginActivationClick );
Copy link
Member

Choose a reason for hiding this comment

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

Why does this add a listener for the entire document instead of adding listeners to the buttons specifically?

I feel like that would be a more reasonable choice, as it avoids having the listener fire for more or less every click on the page. Since the buttons are present in the HTML response from the beginning, I don't see a need for listening on document.

} )();
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 @@ -431,8 +431,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
201 changes: 201 additions & 0 deletions plugins/performance-lab/includes/admin/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?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.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
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 endpoint is not a noun, it may make more sense as /plugin:activate. See for example in Optimization Detective:

/**
* Route for storing a URL Metric.
*
* Note the `:store` 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. Namely, submitting a POST request to this endpoint will either
* create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug.
*
* @link https://google.aip.dev/136
* @var string
*/
const OD_URL_METRICS_ROUTE = '/url-metrics:store';

Suggested change
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
/**
* Route for activating plugin.
*
* 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.
*
* @link https://google.aip.dev/136
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/plugin:activate;

Copy link
Member

Choose a reason for hiding this comment

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

Indeed this is not a great resource URI for a REST URL, as it's not truly RESTful. REST endpoints should be centered around a resource, in this case a plugin.

But even better would be something like: /plugins/<slug>:activate. This would also be more intuitive in that the required parameter slug would be directly in the route path.

Suggested change
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):activate';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@felixarntz I was just curious what should be the criteria for deciding whether to add the slug to the URL itself or include it in the body.

Copy link
Member

@felixarntz felixarntz Nov 14, 2024

Choose a reason for hiding this comment

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

@b1ink0 It's one of the best practices for designing RESTful APIs, they should be centered around resources. See for example https://google.aip.dev/121 and https://google.aip.dev/122. (While these links are for Google's best practices, the best practices are not really specific to Google.)

By centering the API endpoints and their names around resources and their hierarchies, you future-proof the API to remain consistent and intuitive even when you add more and more features to it.

Here, the resource is a plugin. That's why using something like /plugins/<slug> as foundation for the routes is a good starting point. Such an endpoint could be used to get data for a specific plugin, or update data for a plugin for example.

Activating a plugin is not one of the CRUD operations though, so that's where a custom method is needed. See https://google.aip.dev/136 in that regard: That's how /plugins/<slug>:activate makes sense.

For example, if we later needed a method to deactivate a plugin, that would be straightforward via /plugins/<slug>:deactivate. If we used /activate-plugin, this would need to be /deactivate-plugin. That's still somewhat consistent but not centered around the resource and less intuitive. For example, nothing in the name tells you that you have to provide a slug. Additionally, even if you guessed that some identifier for the plugin would be needed, you wouldn't know whether it needs to be a parameter called slug or id or identifier for example. The resource identifier being part of the endpoint path makes that easier to work with.

Copy link
Member

Choose a reason for hiding this comment

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

@westonruter Following up on the other thread (since it probably makes sense to center that conversation about the endpoint naming): Per https://google.aip.dev/122, I think what would make most sense for the settings URL would be to simply be a value on the object returned by GET /plugins/<slug>.

A settings URL is not part of a collection, because a plugin does not have multiple settings URL. So GET /plugins/<slug>/settings-url would not be right. Because it's not an action (as you previously stated), GET /plugins/<slug>:settings-url wouldn't be right either.

The settings URL is one scalar piece of data for a plugin, so it makes most sense to be exposed on a plugin object. Obviously for this PR we don't care about creating a fully fledged GET /plugins/<slug> endpoint, but that's fine. We can simply for now return an object that e.g. only has slug and settingsUrl properties. The schema for such an object could be extended in the future in case we ever found a need for it, without backward compatibility breaks.

So I think going with POST /plugins/<slug>:activate and GET /plugins/<slug> (the latter of which return an object that includes the settings URL) is best in line with the API design guidelines.

Copy link
Member

Choose a reason for hiding this comment

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

@felixarntz Additionally, there is also already a way to activate a plugin using the REST API by setting the status field on the entity to active, without resorting to a special endpoint naming scheme: https://developer.wordpress.org/rest-api/reference/plugins/#update-a-plugin

However, reusing this endpoint is more complicated since we also automatically install the plugin prior to activation, as well as installing and activating any dependencies.

So maybe we should keep our own endpoint for installation and activation but instead of calling it plugin:activate we call it feature:activate to differentiate it from general plugin activation, as these plugins represent performance features.

In the end, this API is almost guaranteed to be used 100% internally and so we don't really need to stress about it being getting it right forever.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think the existing plugin endpoints in Core are a problem. We use a different namespace specific to Performance Lab, so I wouldn't worry about that. It's IMO not unreasonable to provide custom endpoints for similar use-cases under a different namespace when the Core endpoints do not satisfy the requirements we have.

Regarding updating a status field to active, that's how they chose to build it, but doesn't seem that great to me per https://google.aip.dev/216.

That said, I'd be okay using the terminology "features" instead of "plugins" for our endpoints, since that's also what we use in the UI, and I agree it can help differentiate.

Copy link
Member

Choose a reason for hiding this comment

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

So let's go with POST /features/<slug>:activate and GET /features/<slug> then?

Copy link
Member

Choose a reason for hiding this comment

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

Where GET /features/<slug> returns an object with one key, settingsUrl which may be either a string or null, right? Sounds good.

Copy link
Member

Choose a reason for hiding this comment

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

Exactly.

If we like, we could also include e.g. slug in the response object as that's simple enough and clarifies the intent of the endpoint even more (that it's supposed to return plugin information, so not necessarily just the settings URL).


/**
* Route for fetching plugin settings URL.
*
westonruter marked this conversation as resolved.
Show resolved Hide resolved
* @var string
*/
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url/(?P<slug>[a-z0-9_-]+)';

Copy link
Member

Choose a reason for hiding this comment

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

See above, better would be:

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):settings-url';

This way the two routes are also consistently centered around a specific plugin.

Copy link
Member

Choose a reason for hiding this comment

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

But in this case it's just a regular REST API endpoint. No need for any workaround for a non-RESTy POST request, right?

Copy link
Member

Choose a reason for hiding this comment

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

How do you mean? How is this a workaround?

Copy link
Member

@westonruter westonruter Nov 14, 2024

Choose a reason for hiding this comment

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

I mean, as I understand, a workaround like this is needed when the REST API endpoint is not a noun and/or the existing HTTP methods don't make semantic sense. According to AIP-136 on custom methods:

Resource-oriented design (AIP-121) uses custom methods to provide a means to express arbitrary actions that are difficult to model using only the standard methods.

What we discussed above in https://github.com/WordPress/performance/pull/1646/files#r1841160336 is to add a verb to the endpoint for use with POST. But here for the settings URL, it's a classic REST API endpoint, like most other REST API endpoints in WordPress, where we're just GET a noun. So so the normal REST design makes more sense to me.

Copy link
Member

Choose a reason for hiding this comment

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

That's a good point. Though the current naming still seems incorrect in that it's the settings URL of a specific plugin. So a more appropriate name would be:

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+)/settings-url';

This way it is a resource, but under the plugin it belongs to. Arguably with that approach, the 404 error if there's no settings URL makes more sense too.

Copy link
Member

Choose a reason for hiding this comment

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

See #1646 (comment), discard my previous comment.

Let's continue that discussion on the other thread, to center the conversation about endpoint naming in one place.


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_ACTIVATE_PLUGIN_ROUTE,
array(
'methods' => 'POST',
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Plugin slug of plugin that needs to be activated.', 'performance-lab' ),
'required' => true,
westonruter marked this conversation as resolved.
Show resolved Hide resolved
),
),
'callback' => 'perflab_handle_activate_plugin',
'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_PLUGIN_SETTINGS_URL_ROUTE,
array(
'methods' => 'POST',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Plugin slug of plugin whose settings URL is needed.', 'performance-lab' ),
'required' => true,
westonruter marked this conversation as resolved.
Show resolved Hide resolved
),
),
'callback' => 'perflab_handle_get_plugin_settings_url',
'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 settings URL on this site.', 'performance-lab' ) );
},
)
);
}
add_action( 'rest_api_init', 'perflab_register_endpoint' );

/**
* Handles REST API request to activate plugin.
*
* @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_activate_plugin( WP_REST_Request $request ) {
Copy link
Member

Choose a reason for hiding this comment

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

Now that we expose this as a REST endpoint, I wonder whether we should restrict it to plugins that are part of the Performance Lab program. Otherwise we may need to deal with other implications related to arbitrary other plugins. I feel like limiting to our own would be a good idea, for both of these endpoints.

For example, we may make certain assumptions about our plugins, that aren't reasonable to make for any plugin.

Copy link
Member

Choose a reason for hiding this comment

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

This is already restricted to the PL plugins, is it not? It's using perflab_sanitize_plugin_slug() which returns null if it is not a valid PL plugin slug. But I also suggested an alternative in https://github.com/WordPress/performance/pull/1646/files#r1840894846

So I think this is fine.

require_once ABSPATH . 'wp-admin/includes/plugin.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';

// Require to make helper functions available.
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';

$params = $request->get_json_params();

// Ensure the 'slug' parameter is present.
if ( ! isset( $params['slug'] ) ) {
return new WP_Error(
'missing_parameter',
__( 'Missing required parameter "slug".', 'performance-lab' ),
array( 'status' => 400 )
);
}
westonruter marked this conversation as resolved.
Show resolved Hide resolved

$plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $params['slug'] ) );
if ( null === $plugin_slug ) {
return new WP_Error(
'invalid_plugin',
__( 'Invalid plugin slug provided.', 'performance-lab' ),
array( 'status' => 400 )
);
}
westonruter marked this conversation as resolved.
Show resolved Hide resolved

// Install and activate the plugin and its dependencies.
$result = perflab_install_and_activate_plugin( $plugin_slug );
westonruter 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 settings URL.
*
* @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_get_plugin_settings_url( WP_REST_Request $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';

// Require to make helper functions available.
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';

$params = $request->get_json_params();

// Ensure the 'slug' parameter is present.
if ( ! isset( $params['slug'] ) ) {
return new WP_Error(
'missing_parameter',
__( 'Missing required parameter "slug".', 'performance-lab' ),
array( 'status' => 400 )
);
}

$plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $params['slug'] ) );
if ( null === $plugin_slug ) {
return new WP_Error(
'invalid_plugin',
__( 'Invalid plugin slug provided.', 'performance-lab' ),
array( 'status' => 400 )
);
}
westonruter marked this conversation as resolved.
Show resolved Hide resolved

$plugin_settings_url = perflab_get_plugin_settings_url( $plugin_slug );
westonruter marked this conversation as resolved.
Show resolved Hide resolved
if ( null === $plugin_settings_url ) {
return new WP_REST_Response(
array(
'success' => true,
'pluginSettingsURL' => false,
)
);
Copy link
Member

Choose a reason for hiding this comment

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

I think it would make more sense to return a 404 status code here:

Suggested change
return new WP_REST_Response(
array(
'success' => true,
'pluginSettingsURL' => false,
)
);
return new WP_Error(
'no_settings_url',
__( 'No settings URL', 'performance-lab' ),
array( 'status' => 404 )
);

Copy link
Member

@felixarntz felixarntz Nov 13, 2024

Choose a reason for hiding this comment

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

I disagree here. The caller provides the plugin slug, not an identifier for the settings URL. I'd argue a 404 would only be appropriate if the plugin slug was for a plugin that's not active or doesn't exist. But if the plugin is active and just doesn't provide a settings URL, then it shouldn't be a 404, since the endpoint's sole purpose is to retrieve the settings URL for an active plugin.

Copy link
Member

Choose a reason for hiding this comment

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

The endpoint is for a settings URL entity though. You provide the slug of the plugin to get the settings URL. This suggestion is intended to be paired with https://github.com/WordPress/performance/pull/1646/files#r1840919242 so that if you GET /plugin-settings-url/foo and there is no settings URL for that plugin Foo, then it returns a 404.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we implement the 404 approach browser will log error in console for all the plugins which does not provide settings URL. As the browser logs error for 404 status code returned request even if we wrap it in try catch. How should I approach this issue?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, maybe a 404 is too noisy. The endpoint could just return null in that case as opposed to the URL string.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return new WP_REST_Response(
array(
'success' => true,
'pluginSettingsURL' => false,
)
);
return new WP_REST_Response( null );

Copy link
Member

Choose a reason for hiding this comment

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

With null I have the concern that this may also be the result of a erroring JSON-decode operation.

Maybe we return an object with just pluginSettingsURL key, which is either the URL or false? That feels reasonable to me.

Copy link
Member

Choose a reason for hiding this comment

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

JSON decode where? If in JavaScript then this is JSON.parse() throws an error when a parse error occurs.

I just tried patching the comments endpoint to return bad JSON intentionally:

--- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
@@ -422,6 +422,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
 	 */
 	public function get_item( $request ) {
+		die( '{"BADJSON":' );
+
 		$comment = $this->get_comment( $request['id'] );
 		if ( is_wp_error( $comment ) ) {
 			return $comment;

Then if I do:

await wp.apiFetch({ path: '/wp/v2/comments/1', method: 'GET' })

I get an JS error:

image

I don't feel strongly against wrapping the response in an object, however.

}

return new WP_REST_Response(
array(
'success' => true,
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'pluginSettingsURL' => esc_url_raw( $plugin_settings_url ),
westonruter marked this conversation as resolved.
Show resolved Hide resolved
)
);
westonruter marked this conversation as resolved.
Show resolved Hide resolved
}
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