This commit is contained in:
Javier Casares 2026-06-09 12:56:08 +00:00
commit c2ead8e434
13 changed files with 800 additions and 251 deletions

View file

@ -72,6 +72,16 @@ class Plugin {
*/
private array $client_cache = array();
/**
* Credential field being force-cleared during a direct option update.
*
* Set only for the duration of handle_clear_credential() to bypass the
* keep-existing logic in sanitize_settings() via a priority-11 filter.
*
* @var string|null
*/
private ?string $clearing_field = null;
/**
* Retrieves the instance.
*
@ -100,6 +110,10 @@ class Plugin {
\add_action( 'admin_init', array( $this, 'handle_refresh_quota_request' ) );
\add_action( 'admin_notices', array( $this, 'display_admin_notices' ) );
\add_action( 'network_admin_notices', array( $this, 'display_admin_notices' ) );
\add_action( 'admin_post_robotstxt_smtp_amazonses_clear_access_key', array( $this, 'handle_clear_access_key' ) );
\add_action( 'admin_post_robotstxt_smtp_amazonses_clear_secret_key', array( $this, 'handle_clear_secret_key' ) );
\add_action( 'admin_notices', array( $this, 'display_clear_key_notice' ) );
\add_action( 'network_admin_notices', array( $this, 'display_clear_key_notice' ) );
}
/**
@ -1023,21 +1037,42 @@ class Plugin {
* @return void
*/
public function render_access_key_field(): void {
$settings = $this->get_stored_settings();
$has_access_key = '' !== $settings[ self::OPTION_ACCESS_KEY ];
$placeholder_label = $has_access_key ? \__( 'Leave empty to keep the stored access key.', 'robotstxt-smtp-amazonses' ) : '';
$settings = $this->get_stored_settings();
$has_access_key = '' !== $settings[ self::OPTION_ACCESS_KEY ];
$placeholder = $has_access_key ? \__( 'Leave empty to keep the stored access key.', 'robotstxt-smtp-amazonses' ) : '';
$scope = \is_network_admin() ? 'network' : 'site';
?>
<input
name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_ACCESS_KEY ) ); ?>"
type="text"
id="robotstxt_smtp_amazon_ses_access_key"
class="regular-text"
value=""
autocomplete="off"
placeholder="<?php echo \esc_attr( $placeholder_label ); ?>"
/>
<p class="description"><?php \esc_html_e( 'Provide the IAM access key ID with permissions to send email through Amazon SES. Leave the field empty to retain the existing value.', 'robotstxt-smtp-amazonses' ); ?></p>
<?php
<input
name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_ACCESS_KEY ) ); ?>"
type="text"
id="robotstxt_smtp_amazon_ses_access_key"
class="regular-text"
value=""
autocomplete="off"
placeholder="<?php echo \esc_attr( $placeholder ); ?>"
/>
<?php if ( $has_access_key ) : ?>
<a href="
<?php
echo \esc_url(
\wp_nonce_url(
\add_query_arg(
array(
'action' => 'robotstxt_smtp_amazonses_clear_access_key',
'robotstxt_smtp_scope' => $scope,
),
\admin_url( 'admin-post.php' )
),
'robotstxt_smtp_amazonses_clear_access_key'
)
);
?>
" class="button button-secondary" style="margin-left: 6px;">
<?php \esc_html_e( 'Clear access key', 'robotstxt-smtp-amazonses' ); ?>
</a>
<?php endif; ?>
<p class="description"><?php \esc_html_e( 'Provide the IAM access key ID with permissions to send email through Amazon SES. Leave the field empty to retain the existing value.', 'robotstxt-smtp-amazonses' ); ?></p>
<?php
}
/**
@ -1046,21 +1081,42 @@ class Plugin {
* @return void
*/
public function render_secret_key_field(): void {
$settings = $this->get_stored_settings();
$has_secret_key = '' !== $settings[ self::OPTION_SECRET_KEY ];
$placeholder_label = $has_secret_key ? \__( 'Leave empty to keep the stored secret access key.', 'robotstxt-smtp-amazonses' ) : '';
$settings = $this->get_stored_settings();
$has_secret_key = '' !== $settings[ self::OPTION_SECRET_KEY ];
$placeholder = $has_secret_key ? \__( 'Leave empty to keep the stored secret access key.', 'robotstxt-smtp-amazonses' ) : '';
$scope = \is_network_admin() ? 'network' : 'site';
?>
<input
name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_SECRET_KEY ) ); ?>"
type="password"
id="robotstxt_smtp_amazon_ses_secret_key"
class="regular-text"
value=""
autocomplete="new-password"
placeholder="<?php echo \esc_attr( $placeholder_label ); ?>"
/>
<p class="description"><?php \esc_html_e( 'Enter the secret access key associated with the IAM user. Leave the field empty to retain the existing value.', 'robotstxt-smtp-amazonses' ); ?></p>
<?php
<input
name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_SECRET_KEY ) ); ?>"
type="password"
id="robotstxt_smtp_amazon_ses_secret_key"
class="regular-text"
value=""
autocomplete="new-password"
placeholder="<?php echo \esc_attr( $placeholder ); ?>"
/>
<?php if ( $has_secret_key ) : ?>
<a href="
<?php
echo \esc_url(
\wp_nonce_url(
\add_query_arg(
array(
'action' => 'robotstxt_smtp_amazonses_clear_secret_key',
'robotstxt_smtp_scope' => $scope,
),
\admin_url( 'admin-post.php' )
),
'robotstxt_smtp_amazonses_clear_secret_key'
)
);
?>
" class="button button-secondary" style="margin-left: 6px;">
<?php \esc_html_e( 'Clear secret key', 'robotstxt-smtp-amazonses' ); ?>
</a>
<?php endif; ?>
<p class="description"><?php \esc_html_e( 'Enter the secret access key associated with the IAM user. Leave the field empty to retain the existing value.', 'robotstxt-smtp-amazonses' ); ?></p>
<?php
}
/**
@ -1685,4 +1741,157 @@ class Plugin {
$result = \get_site_transient( self::get_quota_cache_key() );
return is_array( $result ) ? $result : false;
}
/**
* Handles the request to clear the stored Access Key ID.
*
* @since 2.1.6
*
* @return void
*/
public function handle_clear_access_key(): void {
$this->handle_clear_credential(
self::OPTION_ACCESS_KEY,
'robotstxt_smtp_amazonses_clear_access_key',
'access_key'
);
}
/**
* Handles the request to clear the stored Secret Access Key.
*
* @since 2.1.6
*
* @return void
*/
public function handle_clear_secret_key(): void {
$this->handle_clear_credential(
self::OPTION_SECRET_KEY,
'robotstxt_smtp_amazonses_clear_secret_key',
'secret_key'
);
}
/**
* Clears a stored AWS credential from the settings option.
*
* Uses a priority-11 filter on `robotstxt_smtp_sanitized_options` to override
* the keep-existing logic in sanitize_settings() (priority 10) during the direct
* option update, matching the same pattern used by the parent plugin for the SMTP
* password.
*
* @since 2.1.6
*
* @param string $field Settings key to clear (OPTION_ACCESS_KEY or OPTION_SECRET_KEY).
* @param string $nonce_action Nonce action string for this specific clear request.
* @param string $cleared_key Value used in the success redirect query parameter.
*
* @return void
*/
private function handle_clear_credential( string $field, string $nonce_action, string $cleared_key ): void {
$nonce_raw = \filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
$nonce = is_string( $nonce_raw ) ? (string) \wp_unslash( $nonce_raw ) : '';
if ( ! \wp_verify_nonce( $nonce, $nonce_action ) ) {
\wp_die( \esc_html__( 'The link you followed has expired.', 'robotstxt-smtp-amazonses' ) );
}
$scope_raw = \filter_input( INPUT_GET, 'robotstxt_smtp_scope', FILTER_UNSAFE_RAW );
$is_network = ROBOTSTXT_SMTP_IS_MULTISITE
&& is_string( $scope_raw )
&& 'network' === \sanitize_key( \wp_unslash( $scope_raw ) )
&& Settings_Page::is_network_mode_enabled();
$required_cap = $is_network ? 'manage_network_options' : 'manage_options';
if ( ! \current_user_can( $required_cap ) ) {
\wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-smtp-amazonses' ) );
}
if ( $is_network ) {
$settings = \get_site_option( Settings_Page::NETWORK_OPTION_NAME, array() );
$settings = is_array( $settings ) ? $settings : array();
$settings[ $field ] = '';
\update_site_option( Settings_Page::NETWORK_OPTION_NAME, $settings );
$redirect_base = \network_admin_url( 'admin.php' );
$page_slug = 'robotstxt-smtp-network';
} else {
$settings = \get_option( Settings_Page::OPTION_NAME, array() );
$settings = is_array( $settings ) ? $settings : array();
$settings[ $field ] = '';
// sanitize_settings() at priority 10 preserves the existing value when the
// submitted field is empty. Hook at priority 11 to force the empty value after it.
$this->clearing_field = $field;
\add_filter( 'robotstxt_smtp_sanitized_options', array( $this, 'force_clear_credential' ), 11, 1 );
\update_option( Settings_Page::OPTION_NAME, $settings );
\remove_filter( 'robotstxt_smtp_sanitized_options', array( $this, 'force_clear_credential' ), 11 );
$this->clearing_field = null;
$redirect_base = \admin_url( 'admin.php' );
$page_slug = 'robotstxt-smtp';
}
\delete_site_transient( self::get_quota_cache_key() );
\wp_safe_redirect(
\add_query_arg(
array(
'page' => $page_slug,
'robotstxt_smtp_amazonses_cleared' => $cleared_key,
),
$redirect_base
)
);
exit;
}
/**
* Forces a credential field to empty string during a direct option update.
*
* Runs at priority 11, after sanitize_settings() at priority 10, and overrides
* its keep-existing logic. Only active during handle_clear_credential().
*
* @since 2.1.6
*
* @param array<string, mixed> $clean Sanitized option values.
*
* @return array<string, mixed>
*/
public function force_clear_credential( array $clean ): array {
if ( null !== $this->clearing_field ) {
$clean[ $this->clearing_field ] = '';
}
return $clean;
}
/**
* Displays a success notice after an AWS credential has been cleared.
*
* @since 2.1.6
*
* @return void
*/
public function display_clear_key_notice(): void {
$cleared_raw = \filter_input( INPUT_GET, 'robotstxt_smtp_amazonses_cleared', FILTER_SANITIZE_SPECIAL_CHARS );
$cleared_raw = is_string( $cleared_raw ) ? \sanitize_key( $cleared_raw ) : '';
if ( '' === $cleared_raw ) {
return;
}
$screen = \get_current_screen();
if ( null === $screen || false === strpos( $screen->id, 'robotstxt-smtp' ) ) {
return;
}
if ( 'access_key' === $cleared_raw ) {
$message = \__( 'The Amazon SES Access Key ID has been cleared.', 'robotstxt-smtp-amazonses' );
} elseif ( 'secret_key' === $cleared_raw ) {
$message = \__( 'The Amazon SES Secret Access Key has been cleared.', 'robotstxt-smtp-amazonses' );
} else {
return;
}
printf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
\esc_html( $message )
);
}
}