From 4c323c2fd22f5973ca45f5e65870f734bd037018 Mon Sep 17 00:00:00 2001 From: Harshit M Shingala Date: Wed, 14 Oct 2026 13:46:18 +0530 Subject: [PATCH] Connectors: Prompt for filesystem credentials when installing connector plugins. When the filesystem method requires credentials which have not been stored (for example, when `FS_METHOD` is `ftpext`), installing a connector plugin from Settings > Connectors fails, as the REST API plugins endpoint cannot accept filesystem credentials. This enqueues the `updates` script and prints the "Connection Information" modal on the Connectors screen, matching Plugins > Add Plugin. It also exposes a `filesystemCredentialsRequired` flag to the Connectors script module, so that the client can install via `wp.updates.installPlugin()` in exactly the cases where the REST API would fail. See #65223. Co-Authored-By: Claude Opus 5.5 --- src/wp-admin/options-connectors.php | 9 +++ src/wp-includes/connectors.php | 23 +++++++ ...ConnectorsGetConnectorScriptModuleData.php | 61 +++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/src/wp-admin/options-connectors.php b/src/wp-admin/options-connectors.php index ddad78f4f135f..60e1e6f9c6001 100644 --- a/src/wp-admin/options-connectors.php +++ b/src/wp-admin/options-connectors.php @@ -32,9 +32,18 @@ // Set parent file for menu highlighting. $parent_file = 'options-general.php'; +/* + * Connector plugins are installed via `wp.updates.installPlugin()` when the + * filesystem method requires credentials which have not been stored. + */ +wp_enqueue_script( 'updates' ); + require_once ABSPATH . 'wp-admin/admin-header.php'; // Render the Connectors page. wp_options_connectors_wp_admin_render_page(); +wp_print_request_filesystem_credentials_modal(); +wp_print_admin_notice_templates(); + require_once ABSPATH . 'wp-admin/admin-footer.php'; diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 9c83a973eba43..dd488a5e9bcbe 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -899,6 +899,7 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { * Exposes connector settings to the connectors-wp-admin script module. * * @since 7.0.0 + * @since 7.2.0 Added the `filesystemCredentialsRequired` flag. * @access private * * @param array $data Existing script module data. @@ -911,6 +912,10 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } + if ( ! function_exists( 'request_filesystem_credentials' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + $connectors = array(); foreach ( wp_get_connectors() as $connector_id => $connector_data ) { $auth = $connector_data['authentication']; @@ -965,6 +970,24 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { ksort( $connectors ); $data['connectors'] = $connectors; $data['isFileModDisabled'] = ! wp_is_file_mod_allowed( 'install_plugins' ); + + /* + * The REST API plugins endpoint rejects install requests when the filesystem + * method requires credentials which have not been stored. In that case, the + * client installs via `wp.updates.installPlugin()` instead, which prompts for + * the credentials with the "Connection Information" modal. + * + * This mirrors WP_REST_Plugins_Controller::is_filesystem_available(). + */ + $data['filesystemCredentialsRequired'] = false; + if ( ! $data['isFileModDisabled'] && 'direct' !== get_filesystem_method() ) { + ob_start(); + $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); + ob_end_clean(); + + $data['filesystemCredentialsRequired'] = ! $filesystem_credentials_are_stored; + } + return $data; } add_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' ); diff --git a/tests/phpunit/tests/connectors/wpConnectorsGetConnectorScriptModuleData.php b/tests/phpunit/tests/connectors/wpConnectorsGetConnectorScriptModuleData.php index b9b5014affdd1..e2af197d68467 100644 --- a/tests/phpunit/tests/connectors/wpConnectorsGetConnectorScriptModuleData.php +++ b/tests/phpunit/tests/connectors/wpConnectorsGetConnectorScriptModuleData.php @@ -209,4 +209,65 @@ public function test_environment_variable_password_may_contain_colons(): void { $this->assertSame( 'env', $auth['keySource'] ); $this->assertTrue( $auth['isConnected'] ); } + + /** + * @ticket 65223 + */ + public function test_filesystem_credentials_not_required_for_direct_filesystem_method(): void { + $this->set_filesystem_method( 'direct' ); + + $data = _wp_connectors_get_connector_script_module_data( array() ); + + $this->assertFalse( $data['filesystemCredentialsRequired'] ); + } + + /** + * @ticket 65223 + */ + public function test_filesystem_credentials_required_when_credentials_are_not_stored(): void { + $this->set_filesystem_method( 'ftpext' ); + + $data = _wp_connectors_get_connector_script_module_data( array() ); + + $this->assertTrue( $data['filesystemCredentialsRequired'] ); + } + + /** + * @ticket 65223 + */ + public function test_filesystem_credentials_not_required_when_credentials_are_stored(): void { + $this->set_filesystem_method( 'ftpext' ); + add_filter( 'request_filesystem_credentials', '__return_true' ); + + $data = _wp_connectors_get_connector_script_module_data( array() ); + + $this->assertFalse( $data['filesystemCredentialsRequired'] ); + } + + /** + * @ticket 65223 + */ + public function test_filesystem_credentials_not_required_when_file_modifications_are_disallowed(): void { + $this->set_filesystem_method( 'ftpext' ); + add_filter( 'file_mod_allowed', '__return_false' ); + + $data = _wp_connectors_get_connector_script_module_data( array() ); + + $this->assertTrue( $data['isFileModDisabled'] ); + $this->assertFalse( $data['filesystemCredentialsRequired'] ); + } + + /** + * Forces the filesystem method returned by get_filesystem_method(). + * + * @param string $method Filesystem method. + */ + private function set_filesystem_method( string $method ): void { + add_filter( + 'filesystem_method', + static function () use ( $method ) { + return $method; + } + ); + } }