Skip to content

Performance Lab: Show filesystem credentials modal on plugin install#2482

Draft
westonruter wants to merge 2 commits into
trunkfrom
fix/install-fs-credentials
Draft

Performance Lab: Show filesystem credentials modal on plugin install#2482
westonruter wants to merge 2 commits into
trunkfrom
fix/install-fs-credentials

Conversation

@westonruter
Copy link
Copy Markdown
Member

@westonruter westonruter commented May 15, 2026

Summary

Fixes the Performance Features install/activate flow when FS_METHOD is not direct — for example, ftpext / ftpsockets.

To reproduce on trunk: with define( 'FS_METHOD', 'ftpsockets' ); in wp-config.php, visit Settings → Performance and click Activate on a not-yet-installed feature plugin (e.g. Performant Translations). The click appears to do nothing — the button briefly says "Activating…" and then reverts. Under the hood POST /wp-json/performance-lab/v1/features/{slug}:activate returns 404 plugin_not_found. The chain of failures is silent:

  1. Plugin_Upgrader::install() calls WP_Upgrader::fs_connect(), which calls request_filesystem_credentials( false, … ) — that returns false when credentials aren't available.
  2. fs_connect() then returns false without populating $skin->errors, so Plugin_Upgrader::install() returns false (not a WP_Error).
  3. perflab_install_and_activate_plugin() falls through all of its is_wp_error / $skin->get_errors()->has_errors() checks, then hits the get_plugins( '/' . $plugin_slug ) empty check and returns the misleading plugin_not_found error.

Plugins > Add Plugin handles the same case by displaying the standard "Connection Information" modal. This PR mirrors that flow on the Performance Features screen.

This is the Performance Lab adaptation of the same fix made for the Connectors screen in Gutenberg in WordPress/gutenberg#78367 (for Core-65223).

Changes

  • plugins/performance-lab/includes/admin/load.php

    • perflab_load_features_page() registers an admin_footer action to print the standard filesystem credentials modal.
    • perflab_enqueue_features_page_scripts() enqueues the updates script and uses wp_add_inline_script (printed 'before' the activation handler) to expose window.perflabPluginActivate.filesystemCredentialsRequired.
    • New perflab_filesystem_credentials_required() helper returns true when get_filesystem_method() !== 'direct' and request_filesystem_credentials( self_admin_url() ) is false.
  • plugins/performance-lab/includes/admin/plugin-activate-ajax.js

    • When filesystemCredentialsRequired is true, the install is routed through a new installPluginViaWpUpdates() helper that wraps wp.updates.installPlugin() and resolves with 'installed' | 'canceled'. Cancellation is a normal control-flow outcome of the promise — not an error message string — so the caller branches on the discriminated return value, not on error.message.
    • Real install failures (download error, missing wp.updates, etc.) still reject and flow through the existing } catch { path unchanged.
    • After the install succeeds, the existing REST :activate call performs the activation step (which no longer needs filesystem access).

The plugin-activate-ajax.min.js is generated by npm run build:plugin:performance-lab and is gitignored, so it isn't part of the diff.

Test plan

  • In wp-config.php, set define( 'FS_METHOD', 'ftpsockets' ); (or 'ftpext'). See Trac comment for how to set this up.
  • Have an FTP/SSH server reachable with credentials you know.
  • Make sure performant-translations is not installed, then visit Settings → Performance.
  • Click Activate on Performant Translations. Confirm the standard Connection Information modal opens (matching Plugins > Add Plugin).
  • Enter valid FTP credentials and click Proceed. Confirm the plugin installs, activates, and the button flips to Active.
  • Re-test with invalid credentials — the modal shows the standard error and stays open.
  • Re-test by cancelling the modal — the button reverts to Activate with no error snackbar.
  • Re-test on a direct filesystem environment — the install proceeds via REST as before, no modal.
  • Re-test activating an already-installed feature plugin — the FS modal is skipped (no install needed) and the existing REST activate runs as before.

How AI was used

This fix was implemented with Claude Code as an adaptation of the Connectors-screen fix made for Gutenberg in WordPress/gutenberg#78367.

The Performance Lab work was kicked off with this prompt:

ok, excellent. Now, let's shift gears and implement the same fix for the Performance Lab plugin. You can reproduce this by going to http://localhost:8000/wp-admin/options-general.php?page=performance-lab in Chrome and attempting to install the "Performant Translations" plugin. You'll see that upon clicking "Activate" (which normally installs behind the scenes) actually results in nothing happening, while the REST API responses have errors behind the scenes. The repo you'll be working with here is at src/wp-content/plugins/performance

Summary of the conversation that followed:

  1. Investigation. Traced plugin-activate-ajax.jsPOST /performance-lab/v1/features/{slug}:activateperflab_install_and_activate_plugin()Plugin_Upgrader::install(). Confirmed via WP_Upgrader::fs_connect() that the upgrader silently returns false when filesystem credentials are missing, with no error set on the skin — which is why the helper falls through to the plugin_not_found branch.
  2. Reproduction. Clicked Activate on Performant Translations under FS_METHOD=ftpsockets and captured the POST … 404 {"code":"plugin_not_found",…} response confirming the silent failure mode.
  3. Implementation. Mirrored the Gutenberg/Connectors approach: enqueue wp.updates and print the standard credentials modal on the Performance Features page; pass a filesystemCredentialsRequired flag from PHP via wp_add_inline_script; have the activation JS route through wp.updates.installPlugin() first when that flag is set, then continue to the existing REST :activate call.
  4. Design feedback round. Initial draft signaled "user cancelled" by rejecting with new Error( 'Filesystem credentials request canceled.' ) and matching the message string at the call site. Reviewer flagged this as brittle. Redesigned installPluginViaWpUpdates() to resolve with the discriminated string 'installed' | 'canceled' instead — cancel is a normal resolved value, real errors still reject. Documented the return type via JSDoc.
  5. Static analysis fixes. PHPStan flagged empty( $stored ) (forbidden by the project's rule level); replaced with false === $stored. ESLint required JSDoc alignment fixes, auto-applied. PHPCS and TypeScript checks clean.
  6. Verification. Rebuilt the min.js with npm run build:plugin:performance-lab. (First in-browser test ran the old cached JS; a hard reload picked up the new build.) Clicked Activate, FTP modal opened, entered valid credentials, plugin installed via FTP and activated, button flipped to Active. Verified wp plugin status performant-translations returned Status: Active. Cleaned up by deleting the test install and the ftp_credentials option.

Relationship to the Gutenberg PR's review feedback

This PR is a sibling of WordPress/gutenberg#78367, which received Copilot review feedback after this implementation was written. Because the Performance Lab work post-dated (and learned from) that review, the corrected patterns were applied here from the start — no follow-up changes are needed:

  • .on() not .one() for the install handlersinstallPluginViaWpUpdates() registers wp-plugin-install-success/-error/credential-modal-cancel with .on() + an explicit slug-filtered cleanup(), so a concurrent install of a different plugin can't consume a one-shot handler and strand the promise. (Gutenberg fix: discussion_r3250956734.)
  • Cancel as a discriminated outcome, not an error-message string — resolves 'installed' | 'canceled' rather than throwing/matching a hard-coded message. (Gutenberg fix: discussion_r3250956865.)
  • No empty() — uses false === $stored to satisfy this repo's PHPStan rule level.
  • The Gutenberg pluginBasename activation note (discussion_r3250928328) does not apply here: Performance Lab activates via its own slug-based POST /performance-lab/v1/features/{slug}:activate endpoint, not the core /wp/v2/plugins path.

When `FS_METHOD` is not `direct` and FTP/SSH credentials have not been
stored, `Plugin_Upgrader::install()` returns `false` without raising a
`WP_Error`, so the `:activate` REST endpoint falls through with a
generic `plugin_not_found` 404 and the click on Activate appears to do
nothing. The standard `Plugins > Add Plugin` flow handles the same
case by displaying the "Connection Information" modal.

Print that modal on the Performance Features screen, expose
`filesystemCredentialsRequired` to the activation JS, and route the
install through `wp.updates.installPlugin()` when credentials are
required so the legacy AJAX install path supplies them. The install
helper resolves with `'installed' | 'canceled'` so cancel is a normal
control-flow outcome rather than an exception. After install, the
existing REST `:activate` call performs the activation step
(which no longer needs filesystem access).
@westonruter westonruter added this to the performance-lab n.e.x.t milestone May 15, 2026
@westonruter westonruter added [Type] Bug An existing feature is broken [Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only labels May 15, 2026
@westonruter westonruter moved this from Not Started/Backlog 📆 to In Progress 🚧 in WP Performance Ongoing May 15, 2026
@github-project-automation github-project-automation Bot moved this to Not Started/Backlog 📆 in WP Performance Ongoing May 15, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.58%. Comparing base (347597f) to head (237f7f9).
⚠️ Report is 5 commits behind head on trunk.

Additional details and impacted files
@@            Coverage Diff             @@
##            trunk    #2482      +/-   ##
==========================================
+ Coverage   69.33%   69.58%   +0.24%     
==========================================
  Files          90       90              
  Lines        7749     7773      +24     
==========================================
+ Hits         5373     5409      +36     
+ Misses       2376     2364      -12     
Flag Coverage Δ
multisite 69.58% <100.00%> (+0.24%) ⬆️
single 35.81% <50.00%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Cover the code paths added for the FS_METHOD-not-direct install flow:

- perflab_load_features_page() registers the admin_footer modal printer.
- perflab_enqueue_features_page_scripts() enqueues `updates` and prints
  the window.perflabPluginActivate inline data.
- perflab_filesystem_credentials_required() returns false for the
  `direct` method, true when credentials are missing, and false when
  credentials are available (request_filesystem_credentials filter).
- perflab_print_filesystem_credentials_modal() prints nothing for the
  `direct` method and the dialog markup when credentials are required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only [Type] Bug An existing feature is broken

Projects

Status: In Progress 🚧

Development

Successfully merging this pull request may close these issues.

1 participant