diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a71e4f95e..c0df50088d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,17 @@ All notable changes to this project will be documented in this file, per [the Ke ## [Unreleased] - diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 9ed56d97d7..948267e102 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -820,9 +820,9 @@ h2 .nav-tab.ep-credentials-tab { padding-left: 0; } - .ep-credentials .form-table td input, + .ep-credentials .form-table td input:not([type="checkbox"]), .ep-credentials .form-table td select, - .ep-credentials-general .form-table td input, + .ep-credentials-general .form-table td input:not([type="checkbox"]), .ep-credentials-general .form-table td select { width: 250px; } diff --git a/includes/classes/ElementorUtils.php b/includes/classes/ElementorUtils.php index 075b182cd6..161242a29a 100644 --- a/includes/classes/ElementorUtils.php +++ b/includes/classes/ElementorUtils.php @@ -108,7 +108,7 @@ public function get_all_widgets_in_all_templates(): array { if ( ! is_array( $template_content ) ) { continue; } - $all_widgets = array_merge( + $all_widgets = array_merge( $all_widgets, $this->recursively_get_inner_widgets( $template_content ) ); diff --git a/includes/classes/Screen/Settings.php b/includes/classes/Screen/Settings.php index 0e1dacf6ac..3f5939e13d 100644 --- a/includes/classes/Screen/Settings.php +++ b/includes/classes/Screen/Settings.php @@ -97,11 +97,23 @@ public function action_admin_init() { Utils\update_option( 'ep_host', $host ); } - if ( isset( $post['ep_credentials'] ) ) { - $credentials = ( isset( $post['ep_credentials'] ) ) ? Utils\sanitize_credentials( $post['ep_credentials'] ) : [ - 'username' => '', - 'token' => '', - ]; + if ( isset( $post['ep_credentials'] ) && ( ! defined( 'EP_CREDENTIALS' ) || ! EP_CREDENTIALS ) ) { + if ( ! empty( $post['ep_remove_token'] ) ) { + $username = isset( $post['ep_credentials']['username'] ) + ? sanitize_text_field( $post['ep_credentials']['username'] ) + : ( $this->prev_ep_credentials['username'] ?? '' ); + $credentials = [ + 'username' => $username, + 'token' => '', + ]; + } else { + $credentials = Utils\sanitize_credentials( $post['ep_credentials'] ); + + // Preserve the existing token if the field was left empty (it is always empty on load). + if ( empty( $credentials['token'] ) ) { + $credentials['token'] = $this->prev_ep_credentials['token']; + } + } Utils\update_option( 'ep_credentials', $credentials ); } diff --git a/includes/partials/settings-page.php b/includes/partials/settings-page.php index 41e44b9dfa..fd8ded1999 100644 --- a/includes/partials/settings-page.php +++ b/includes/partials/settings-page.php @@ -134,12 +134,20 @@ */ if ( apply_filters( 'ep_admin_show_credentials', true ) ) : ?> - disabled type="text" value="" name="ep_credentials[token]" id="ep_token"> + disabled type="password" value="" autocomplete="off" placeholder="" name="ep_credentials[token]" id="ep_token"> + +

+ +

+

-

+

diff --git a/tests/e2e/src/specs/settings-token.spec.ts b/tests/e2e/src/specs/settings-token.spec.ts new file mode 100644 index 0000000000..2254b2b924 --- /dev/null +++ b/tests/e2e/src/specs/settings-token.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '../fixtures.js'; +import { goToAdminPage, isEpIo, wpCli } from '../utils.js'; + +const FAKE_TOKEN = 'super-secret-token'; +const FAKE_USERNAME = 'ep-test-user'; +const EPIO_HOST = 'https://elasticpress.io'; + +/** + * Tests for the Subscription Token field on the ElasticPress Settings page. + * + * The token value is never rendered in the DOM and an empty POST value + * preserves the previously stored token. + */ +test.describe('Settings page Subscription Token field', { tag: '@group1' }, () => { + // The EP_HOST constant (set in wp-config by the CI/e2e setup script) wins + // over the ep_host option in get_host(), so the token row would stay + // hidden on the non-EPIO tab. Removing the constant here lets the seeded + // option control is_epio() and renders the credentials row for these tests. + let originalEpHost = ''; + + test.beforeAll(async () => { + if (isEpIo()) { + return; + } + + originalEpHost = (await wpCli('config get EP_HOST', true))?.toString().trim() ?? ''; + + // Drop the constant so the seeded ep_host option below is used, which + // makes is_epio() true via the host pattern and shows the EPIO tab. + await wpCli('config delete EP_HOST'); + + // Seed a known token and force is_epio() to true via the host pattern + // so the credentials row renders on the ElasticPress.io tab. + await wpCli(`option update ep_host '${EPIO_HOST}' --format=json`); + await wpCli( + `option update ep_credentials '{"username":"${FAKE_USERNAME}","token":"${FAKE_TOKEN}"}' --format=json`, + ); + }); + + test.afterAll(async () => { + if (isEpIo()) { + return; + } + + // Clean up the seeded options so other specs are not affected. + await wpCli('option delete ep_host'); + await wpCli('option delete ep_credentials'); + + // Restore the EP_HOST constant removed in beforeAll so subsequent + // specs in the single-worker run see the original host. + if (originalEpHost) { + await wpCli(`config set EP_HOST ${originalEpHost}`); + } + }); + + test('Token value is not exposed in the page', async ({ loggedInPage }) => { + test.skip(isEpIo(), 'Uses locally seeded credentials.'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + const tokenInput = loggedInPage.locator('#ep_token'); + await expect(tokenInput).toBeVisible(); + await expect(tokenInput).toHaveAttribute('type', 'password'); + await expect(tokenInput).toHaveAttribute('autocomplete', 'off'); + await expect(tokenInput).toHaveValue(''); + + // The actual token must not appear anywhere in the rendered HTML. + const pageHtml = await loggedInPage.content(); + expect(pageHtml).not.toContain(FAKE_TOKEN); + }); + + test('Saving without changing the token preserves the stored value', async ({ + loggedInPage, + }) => { + // Saving calls get_elasticsearch_info( true ) against the EP_HOST. On the + // non-EPIO matrix the seeded EPIO host is unreachable, so the handler + // runs reset_settings() and wipes the credentials. This behavior can only + // be exercised against a real connected EPIO account. + test.skip(!isEpIo(), 'Requires a live EPIO connection.'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + // Submit the form with the token field left untouched. + await loggedInPage.click('#submit'); + await loggedInPage.waitForLoadState('networkidle'); + + const stored = await wpCli('option get ep_credentials --format=json'); + expect(stored?.toString()).toContain(FAKE_TOKEN); + }); + + test('Saving a new token value updates the stored value', async ({ loggedInPage }) => { + // See note above: saving requires a reachable EPIO host. + test.skip(!isEpIo(), 'Requires a live EPIO connection.'); + + const newToken = 'rotated-token'; + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + await loggedInPage.fill('#ep_token', newToken); + await loggedInPage.click('#submit'); + await loggedInPage.waitForLoadState('networkidle'); + + const stored = await wpCli('option get ep_credentials --format=json'); + expect(stored?.toString()).toContain(newToken); + expect(stored?.toString()).not.toContain(FAKE_TOKEN); + }); + + test('Checking the remove token checkbox clears the stored value', async ({ loggedInPage }) => { + test.skip(isEpIo(), 'Uses locally seeded credentials.'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + await loggedInPage.check('#ep_remove_token'); + await loggedInPage.click('#submit'); + await loggedInPage.waitForLoadState('networkidle'); + + const stored = await wpCli('option get ep_credentials --format=json'); + expect(stored?.toString()).not.toContain(FAKE_TOKEN); + expect(stored?.toString()).toContain('"token":""'); + }); +}); diff --git a/tests/php/screen/TestSettings.php b/tests/php/screen/TestSettings.php index 11ed3032f1..7ca2c4b547 100644 --- a/tests/php/screen/TestSettings.php +++ b/tests/php/screen/TestSettings.php @@ -108,6 +108,149 @@ public function test_action_admin_init_wrong_host() { $this->assertNotContains( 'ep_host', $_POST ); } + /** + * Test that an empty token in POST preserves the stored token. + * + * The token field is always empty on page load, so a save without a new + * value must not wipe the existing token. + * + * @group screen + * @group settings-screen + */ + public function test_action_admin_init_empty_token_preserves_stored_token() { + global $_POST; + + if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) { + $this->markTestSkipped( 'EP_CREDENTIALS constant overrides the option.' ); + } + + $previous_epio_environment = getenv( 'IS_EPIO_ENVIRONMENT' ); + // Make is_epio() true so get_epio_credentials() reads the option. + putenv( 'IS_EPIO_ENVIRONMENT=1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + + try { + $settings = new Settings(); + + Utils\update_option( + 'ep_credentials', + [ + 'username' => 'u', + 'token' => 'secret', + ] + ); + + $_POST = [ + 'ep_settings_nonce' => wp_create_nonce( 'elasticpress_settings' ), + 'ep_language' => 'site-default', + 'ep_host' => Utils\get_host(), + 'ep_credentials' => [ + 'username' => 'u', + 'token' => '', + ], + ]; + + $settings->action_admin_init(); + + $this->assertSame( 'secret', Utils\get_option( 'ep_credentials' )['token'] ); + } finally { + putenv( false === $previous_epio_environment ? 'IS_EPIO_ENVIRONMENT' : "IS_EPIO_ENVIRONMENT={$previous_epio_environment}" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + } + } + + /** + * Test that a new token value is saved. + * + * @group screen + * @group settings-screen + */ + public function test_action_admin_init_new_token_is_saved() { + global $_POST; + + if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) { + $this->markTestSkipped( 'EP_CREDENTIALS constant overrides the option.' ); + } + + $previous_epio_environment = getenv( 'IS_EPIO_ENVIRONMENT' ); + // Make is_epio() true so get_epio_credentials() reads the option. + putenv( 'IS_EPIO_ENVIRONMENT=1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + + try { + $settings = new Settings(); + + Utils\update_option( + 'ep_credentials', + [ + 'username' => 'u', + 'token' => 'old', + ] + ); + + $_POST = [ + 'ep_settings_nonce' => wp_create_nonce( 'elasticpress_settings' ), + 'ep_language' => 'site-default', + 'ep_host' => Utils\get_host(), + 'ep_credentials' => [ + 'username' => 'u', + 'token' => 'new-token', + ], + ]; + + $settings->action_admin_init(); + + $this->assertSame( 'new-token', Utils\get_option( 'ep_credentials' )['token'] ); + } finally { + putenv( false === $previous_epio_environment ? 'IS_EPIO_ENVIRONMENT' : "IS_EPIO_ENVIRONMENT={$previous_epio_environment}" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + } + } + + /** + * Test that checking the "remove token" checkbox clears the stored token. + * + * @group screen + * @group settings-screen + */ + public function test_action_admin_init_remove_token_checkbox_clears_stored_token() { + global $_POST; + + if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) { + $this->markTestSkipped( 'EP_CREDENTIALS constant overrides the option.' ); + } + + $previous_epio_environment = getenv( 'IS_EPIO_ENVIRONMENT' ); + // Make is_epio() true so get_epio_credentials() reads the option. + putenv( 'IS_EPIO_ENVIRONMENT=1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + + try { + $settings = new Settings(); + + Utils\update_option( + 'ep_credentials', + [ + 'username' => 'u', + 'token' => 'secret', + ] + ); + + $_POST = [ + 'ep_settings_nonce' => wp_create_nonce( 'elasticpress_settings' ), + 'ep_language' => 'site-default', + 'ep_host' => Utils\get_host(), + 'ep_credentials' => [ + 'username' => 'u', + 'token' => '', + ], + 'ep_remove_token' => '1', + ]; + + $settings->action_admin_init(); + + $this->assertSame( '', Utils\get_option( 'ep_credentials' )['token'] ); + $this->assertSame( 'u', Utils\get_option( 'ep_credentials' )['username'] ); + } finally { + putenv( false === $previous_epio_environment ? 'IS_EPIO_ENVIRONMENT' : "IS_EPIO_ENVIRONMENT={$previous_epio_environment}" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + } + } + /** * Test the `add_validation_notice` method *