Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions plugins/performance-lab/includes/admin/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ function perflab_load_features_page(): void {

// Handle style for settings page.
add_action( 'admin_head', 'perflab_print_features_page_style' );

// Print the filesystem credentials modal in the footer so the activation
// flow can surface it when FS_METHOD is not 'direct'.
add_action( 'admin_footer', 'perflab_print_filesystem_credentials_modal' );
}

/**
* Prints the filesystem credentials modal on the Performance Features screen.
*
* When `FS_METHOD` is anything other than 'direct' and FTP/SSH credentials
* have not been stored, `Plugin_Upgrader::install()` returns false without
* raising a WP_Error, so the REST activation endpoint falls through with a
* generic `plugin_not_found` response and the click appears to do nothing.
* Mirroring the markup that Plugins > Add Plugin already provides lets the
* activation JS fall back to `wp.updates.installPlugin()` to surface the
* standard "Connection Information" dialog.
*
* @since n.e.x.t
*/
function perflab_print_filesystem_credentials_modal(): void {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/template.php';

wp_print_request_filesystem_credentials_modal();
wp_print_admin_notice_templates();
}

/**
Expand Down Expand Up @@ -401,6 +426,10 @@ function perflab_enqueue_features_page_scripts(): void {
wp_enqueue_style( 'thickbox' );
wp_enqueue_script( 'plugin-install' );

// Needed for the filesystem credentials modal fallback when FS_METHOD is
// not 'direct'. See perflab_print_filesystem_credentials_modal().
wp_enqueue_script( 'updates' );

// Enqueue plugin activate AJAX script and localize script data.
wp_enqueue_script(
'perflab-plugin-activate-ajax',
Expand All @@ -409,6 +438,46 @@ function perflab_enqueue_features_page_scripts(): void {
PERFLAB_VERSION,
true
);

wp_add_inline_script(
'perflab-plugin-activate-ajax',
sprintf(
'window.perflabPluginActivate = %s;',
wp_json_encode(
array(
'filesystemCredentialsRequired' => perflab_filesystem_credentials_required(),
)
)
),
'before'
);
}

/**
* Determines whether the current request needs filesystem credentials in
* order to install a plugin.
*
* Returns true when `get_filesystem_method()` is not 'direct' and FTP/SSH
* credentials have not yet been stored via {@see request_filesystem_credentials()}.
* The activation JS uses this flag to route through `wp.updates.installPlugin()`
* for the standard credentials prompt.
*
* @since n.e.x.t
*
* @return bool Whether filesystem credentials are required.
*/
function perflab_filesystem_credentials_required(): bool {
require_once ABSPATH . 'wp-admin/includes/file.php';

if ( 'direct' === get_filesystem_method() ) {
return false;
}

ob_start();
$stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();

return false === $stored;
}

/**
Expand Down
93 changes: 93 additions & 0 deletions plugins/performance-lab/includes/admin/plugin-activate-ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,89 @@
const { i18n, a11y, apiFetch } = wp;
const { __ } = i18n;

// Whether the current request needs FTP/SSH credentials before the
// plugin can be installed. Set from PHP via wp_add_inline_script().
const filesystemCredentialsRequired = Boolean(
// @ts-ignore
window.perflabPluginActivate?.filesystemCredentialsRequired
);

// Queue to hold pending activation requests.
/** @type {{target: HTMLElement, pluginSlug: string}[]} */
const activationQueue = [];
let isProcessingActivation = false;

/**
* Installs a plugin via wp.updates.installPlugin() so the standard
* "Connection Information" modal handles the FTP/SSH credentials flow.
*
* Resolves with a status describing the outcome:
* - 'installed': the plugin was written to disk.
* - 'canceled': the user closed the credentials modal.
* Rejects only on a genuine install failure (e.g. download error or
* wp.updates not being available on the page).
*
* @param {string} slug Plugin slug.
* @return {Promise<'installed' | 'canceled'>} Outcome of the install.
*/
function installPluginViaWpUpdates( slug ) {
return new Promise( ( resolve, reject ) => {
// @ts-ignore
const updates = window.wp?.updates;
// @ts-ignore
const $ = window.jQuery;
if ( ! updates?.installPlugin || ! $ ) {
reject(
new Error( 'wp.updates.installPlugin is not available.' )
);
return;
}

const cleanup = () => {
$( document ).off( 'wp-plugin-install-success', onSuccess );
$( document ).off( 'wp-plugin-install-error', onError );
$( document ).off( 'credential-modal-cancel', onCancel );
};

/**
* @param {unknown} _event
* @param {{slug: string}} response
*/
const onSuccess = ( _event, response ) => {
if ( response.slug !== slug ) {
return;
}
cleanup();
resolve( 'installed' );
};

/**
* @param {unknown} _event
* @param {{slug: string, errorMessage?: string}} response
*/
const onError = ( _event, response ) => {
if ( response.slug !== slug ) {
return;
}
cleanup();
reject(
new Error( response.errorMessage || 'Install failed.' )
);
};

const onCancel = () => {
cleanup();
resolve( 'canceled' );
};

$( document ).on( 'wp-plugin-install-success', onSuccess );
$( document ).on( 'wp-plugin-install-error', onError );
$( document ).on( 'credential-modal-cancel', onCancel );

updates.installPlugin( { slug } );
} );
}

/**
* Enqueues plugin activation requests and starts processing if not already in progress.
*
Expand Down Expand Up @@ -63,6 +141,21 @@
a11y.speak( __( 'Activating…', 'performance-lab' ) );

try {
// When the filesystem method is not 'direct' and credentials
// have not been stored, the REST endpoint silently fails to
// install the plugin. Route the install through wp.updates so
// the standard "Connection Information" modal handles credential
// entry, then let the REST endpoint perform the activation step
// (which no longer needs filesystem access).
if ( filesystemCredentialsRequired ) {
const outcome = await installPluginViaWpUpdates( pluginSlug );
if ( 'canceled' === outcome ) {
target.classList.remove( 'updating-message' );
target.textContent = __( 'Activate', 'performance-lab' );
return;
}
}

// Activate the plugin/feature via the REST API.
await apiFetch( {
path: `/performance-lab/v1/features/${ pluginSlug }:activate`,
Expand Down
129 changes: 129 additions & 0 deletions plugins/performance-lab/tests/includes/admin/test-load.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,133 @@ public function data_provider_to_test_perflab_sanitize_plugin_slug(): array {
public function test_perflab_sanitize_plugin_slug( $slug, ?string $expected ): void {
$this->assertSame( $expected, perflab_sanitize_plugin_slug( $slug ) );
}

/**
* @covers ::perflab_load_features_page
*/
public function test_perflab_load_features_page_registers_filesystem_credentials_modal(): void {
remove_all_actions( 'admin_footer' );

perflab_load_features_page();

$this->assertSame(
10,
has_action( 'admin_footer', 'perflab_print_filesystem_credentials_modal' )
);
$this->assertSame(
10,
has_action( 'admin_enqueue_scripts', 'perflab_enqueue_features_page_scripts' )
);
}

/**
* @covers ::perflab_enqueue_features_page_scripts
*/
public function test_perflab_enqueue_features_page_scripts_enqueues_updates_and_inline_data(): void {
// Ensure get_filesystem_method() is 'direct' so the inline data is deterministic.
add_filter(
'filesystem_method',
static function (): string {
return 'direct';
}
);

set_current_screen( 'options-general' );

perflab_enqueue_features_page_scripts();

$this->assertTrue( wp_script_is( 'updates', 'enqueued' ) );
$this->assertTrue( wp_script_is( 'perflab-plugin-activate-ajax', 'enqueued' ) );

$inline = wp_scripts()->get_inline_script_data( 'perflab-plugin-activate-ajax', 'before' );
$this->assertStringContainsString( 'window.perflabPluginActivate', $inline );
$this->assertStringContainsString( '"filesystemCredentialsRequired":false', $inline );
}

/**
* @covers ::perflab_filesystem_credentials_required
*/
public function test_perflab_filesystem_credentials_required_is_false_for_direct_method(): void {
add_filter(
'filesystem_method',
static function (): string {
return 'direct';
}
);

$this->assertFalse( perflab_filesystem_credentials_required() );
}

/**
* @covers ::perflab_filesystem_credentials_required
*/
public function test_perflab_filesystem_credentials_required_is_true_when_credentials_missing(): void {
add_filter(
'filesystem_method',
static function (): string {
return 'ftpext';
}
);

// No FTP constants/credentials are available, so request_filesystem_credentials() returns false.
$this->assertTrue( perflab_filesystem_credentials_required() );
}

/**
* @covers ::perflab_filesystem_credentials_required
*/
public function test_perflab_filesystem_credentials_required_is_false_when_credentials_stored(): void {
add_filter(
'filesystem_method',
static function (): string {
return 'ftpext';
}
);

// Simulate stored/available credentials by short-circuiting request_filesystem_credentials().
add_filter(
'request_filesystem_credentials',
static function () {
return array(
'hostname' => 'example.com',
'username' => 'user',
'password' => 'pass',
);
}
);

$this->assertFalse( perflab_filesystem_credentials_required() );
}

/**
* @covers ::perflab_print_filesystem_credentials_modal
*/
public function test_perflab_print_filesystem_credentials_modal_prints_nothing_for_direct_method(): void {
add_filter(
'filesystem_method',
static function (): string {
return 'direct';
}
);

$output = get_echo( 'perflab_print_filesystem_credentials_modal' );

$this->assertStringNotContainsString( 'id="request-filesystem-credentials-dialog"', $output );
}

/**
* @covers ::perflab_print_filesystem_credentials_modal
*/
public function test_perflab_print_filesystem_credentials_modal_prints_dialog_when_credentials_required(): void {
add_filter(
'filesystem_method',
static function (): string {
return 'ftpext';
}
);

$output = get_echo( 'perflab_print_filesystem_credentials_modal' );

$this->assertStringContainsString( 'id="request-filesystem-credentials-dialog"', $output );
}
}
Loading