402 lines
12 KiB
PHP
402 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Admin settings page: API key, store URL, cache TTL, connection test.
|
|
*
|
|
* @package Robotstxt_Manager
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class Robotstxt_Manager_Settings
|
|
*
|
|
* Registers the plugin settings page and all setting fields using the
|
|
* WordPress Settings API. The API key is stored encrypted via
|
|
* Robotstxt_Manager_Encryption and displayed masked in the UI. Provides an
|
|
* AJAX handler for the "Test connection" button.
|
|
*/
|
|
class Robotstxt_Manager_Settings {
|
|
|
|
/**
|
|
* Admin page and menu slug.
|
|
*
|
|
* @var string
|
|
*/
|
|
public const PAGE_SLUG = 'robotstxt-manager-settings';
|
|
|
|
/**
|
|
* Settings API option group.
|
|
*
|
|
* @var string
|
|
*/
|
|
public const OPTION_GROUP = 'robotstxt_manager_settings_group';
|
|
|
|
/**
|
|
* Registers all hooks via the loader.
|
|
*
|
|
* @param Robotstxt_Manager_Loader $loader The plugin hook loader.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function register( Robotstxt_Manager_Loader $loader ): void {
|
|
$loader->add_action( 'admin_menu', $this, 'add_settings_page' );
|
|
$loader->add_action( 'admin_init', $this, 'register_settings' );
|
|
$loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_scripts' );
|
|
$loader->add_action( 'wp_ajax_robotstxt_manager_test_connection', $this, 'handle_test_connection' );
|
|
$loader->add_action( 'wp_ajax_robotstxt_manager_delete_key', $this, 'handle_delete_key' );
|
|
}
|
|
|
|
/**
|
|
* Adds the Settings submenu under the Manager top-level menu.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function add_settings_page(): void {
|
|
add_submenu_page(
|
|
Robotstxt_Manager_Admin::PAGE_SLUG,
|
|
esc_html__( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ),
|
|
esc_html__( 'Settings', 'robotstxt-manager' ),
|
|
'manage_options',
|
|
self::PAGE_SLUG,
|
|
array( $this, 'render_page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers settings, sections, and fields with the Settings API.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function register_settings(): void {
|
|
add_settings_section(
|
|
'robotstxt_manager_connection_section',
|
|
esc_html__( 'Connection', 'robotstxt-manager' ),
|
|
'__return_false',
|
|
self::PAGE_SLUG
|
|
);
|
|
|
|
register_setting(
|
|
self::OPTION_GROUP,
|
|
'robotstxt_manager_store_url',
|
|
array(
|
|
'type' => 'string',
|
|
'sanitize_callback' => 'esc_url_raw',
|
|
'default' => 'https://www.robotstxt.software',
|
|
)
|
|
);
|
|
|
|
add_settings_field(
|
|
'robotstxt_manager_store_url',
|
|
esc_html__( 'Store URL', 'robotstxt-manager' ),
|
|
array( $this, 'render_field_store_url' ),
|
|
self::PAGE_SLUG,
|
|
'robotstxt_manager_connection_section'
|
|
);
|
|
|
|
register_setting(
|
|
self::OPTION_GROUP,
|
|
'robotstxt_manager_api_key',
|
|
array(
|
|
'type' => 'string',
|
|
'sanitize_callback' => array( $this, 'sanitize_api_key' ),
|
|
'default' => '',
|
|
)
|
|
);
|
|
|
|
add_settings_field(
|
|
'robotstxt_manager_api_key',
|
|
esc_html__( 'API Key', 'robotstxt-manager' ),
|
|
array( $this, 'render_field_api_key' ),
|
|
self::PAGE_SLUG,
|
|
'robotstxt_manager_connection_section'
|
|
);
|
|
|
|
add_settings_section(
|
|
'robotstxt_manager_cache_section',
|
|
esc_html__( 'Cache', 'robotstxt-manager' ),
|
|
'__return_false',
|
|
self::PAGE_SLUG
|
|
);
|
|
|
|
register_setting(
|
|
self::OPTION_GROUP,
|
|
'robotstxt_manager_cache_ttl_minutes',
|
|
array(
|
|
'type' => 'integer',
|
|
'sanitize_callback' => array( $this, 'sanitize_cache_ttl' ),
|
|
'default' => 60,
|
|
)
|
|
);
|
|
|
|
add_settings_field(
|
|
'robotstxt_manager_cache_ttl_minutes',
|
|
esc_html__( 'Catalog Cache (minutes)', 'robotstxt-manager' ),
|
|
array( $this, 'render_field_cache_ttl' ),
|
|
self::PAGE_SLUG,
|
|
'robotstxt_manager_cache_section'
|
|
);
|
|
|
|
register_setting(
|
|
self::OPTION_GROUP,
|
|
'robotstxt_manager_delete_data_on_uninstall',
|
|
array(
|
|
'type' => 'boolean',
|
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
|
'default' => false,
|
|
)
|
|
);
|
|
|
|
add_settings_field(
|
|
'robotstxt_manager_delete_data_on_uninstall',
|
|
esc_html__( 'Data on Uninstall', 'robotstxt-manager' ),
|
|
array( $this, 'render_field_delete_on_uninstall' ),
|
|
self::PAGE_SLUG,
|
|
'robotstxt_manager_cache_section'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Enqueues the settings page JS on the correct screen.
|
|
*
|
|
* @param string $hook_suffix The current admin page hook suffix.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function enqueue_scripts( string $hook_suffix ): void {
|
|
if ( ! str_contains( $hook_suffix, self::PAGE_SLUG ) ) {
|
|
return;
|
|
}
|
|
|
|
wp_enqueue_script(
|
|
'robotstxt-manager-settings',
|
|
ROBOTSTXT_MANAGER_URL . 'admin/js/robotstxt-manager-settings.js',
|
|
array( 'jquery' ),
|
|
ROBOTSTXT_MANAGER_VERSION,
|
|
true
|
|
);
|
|
|
|
wp_localize_script(
|
|
'robotstxt-manager-settings',
|
|
'RobotstxtManagerSettings',
|
|
array(
|
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
|
'nonce' => wp_create_nonce( 'robotstxt_manager_test_connection' ),
|
|
'deleteNonce' => wp_create_nonce( 'robotstxt_manager_delete_key' ),
|
|
'i18n' => array(
|
|
'testing' => __( 'Testing…', 'robotstxt-manager' ),
|
|
'testLabel' => __( 'Test connection', 'robotstxt-manager' ),
|
|
'deleting' => __( 'Deleting…', 'robotstxt-manager' ),
|
|
'deleteLabel' => __( 'Delete API key', 'robotstxt-manager' ),
|
|
'deleteConfirm' => __( 'Delete the stored API key? The catalog and subscription data will stop working until a new key is entered.', 'robotstxt-manager' ),
|
|
'deleted' => __( 'API key deleted. Save changes to persist.', 'robotstxt-manager' ),
|
|
'ajaxError' => __( 'An unexpected error occurred.', 'robotstxt-manager' ),
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the settings page.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render_page(): void {
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) );
|
|
}
|
|
|
|
require_once ROBOTSTXT_MANAGER_DIR . 'admin/views/page-settings.php';
|
|
}
|
|
|
|
/**
|
|
* Renders the Store URL field.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render_field_store_url(): void {
|
|
$raw = get_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' );
|
|
$value = is_string( $raw ) ? $raw : 'https://www.robotstxt.software';
|
|
|
|
printf(
|
|
'<input type="url" id="robotstxt_manager_store_url" name="robotstxt_manager_store_url" value="%s" class="regular-text" maxlength="500" placeholder="https://www.robotstxt.software" />',
|
|
esc_attr( $value )
|
|
);
|
|
echo '<p class="description">' . esc_html__( 'Base URL of the remote Plugins Core installation that this site will pull the plugin catalog from.', 'robotstxt-manager' ) . '</p>';
|
|
}
|
|
|
|
/**
|
|
* Renders the API key field. Always empty on render — the stored value
|
|
* is never sent back to the browser. The masked last 4 chars of the
|
|
* stored key are shown as a hint.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render_field_api_key(): void {
|
|
$stored = get_option( 'robotstxt_manager_api_key', '' );
|
|
$has_key = is_string( $stored ) && '' !== $stored;
|
|
$last4 = '';
|
|
|
|
if ( $has_key ) {
|
|
$plain = Robotstxt_Manager_Encryption::decrypt( is_string( $stored ) ? $stored : '' );
|
|
// Only show last-4 if decryption produced a plausible key (36-char UUID).
|
|
if ( '' !== $plain && 36 === strlen( $plain ) ) {
|
|
$last4 = substr( $plain, -4 );
|
|
}
|
|
}
|
|
|
|
echo '<input type="password" id="robotstxt_manager_api_key" name="robotstxt_manager_api_key" value="" class="regular-text" autocomplete="new-password" />';
|
|
|
|
if ( $has_key ) {
|
|
echo '<p class="description">';
|
|
if ( '' !== $last4 ) {
|
|
echo wp_kses_post(
|
|
sprintf(
|
|
/* translators: %s: last 4 characters of the stored API key. */
|
|
__( 'A key is stored (last 4 characters: <code>%s</code>). Leave blank to keep the existing key; enter a new value to replace it.', 'robotstxt-manager' ),
|
|
esc_html( $last4 )
|
|
)
|
|
);
|
|
} else {
|
|
esc_html_e( 'A key is stored but could not be decoded. You can replace it by entering a new value above, or delete it with the button below.', 'robotstxt-manager' );
|
|
}
|
|
echo '</p>';
|
|
} else {
|
|
echo '<p class="description">' . esc_html__( 'Account-level API key issued by the ROBOTSTXT store. Required to authenticate catalog and subscription requests. The value is encrypted before storage.', 'robotstxt-manager' ) . '</p>';
|
|
}
|
|
|
|
// Action buttons.
|
|
echo '<p>';
|
|
|
|
// Test connection: disabled when no key is stored.
|
|
$test_disabled = $has_key ? '' : ' disabled';
|
|
printf( '<button type="button" id="robotstxt-manager-test-connection" class="button"%s>%s</button>', esc_attr( $test_disabled ), esc_html__( 'Test connection', 'robotstxt-manager' ) );
|
|
|
|
// Delete stored key: only shown when a key exists.
|
|
if ( $has_key ) {
|
|
echo ' <button type="button" id="robotstxt-manager-delete-key" class="button button-link-delete">' . esc_html__( 'Delete API key', 'robotstxt-manager' ) . '</button>';
|
|
}
|
|
|
|
echo '<span id="robotstxt-manager-test-result" style="margin-left:8px"></span>';
|
|
echo '</p>';
|
|
}
|
|
|
|
/**
|
|
* Renders the cache TTL field.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render_field_cache_ttl(): void {
|
|
$raw = get_option( 'robotstxt_manager_cache_ttl_minutes', 60 );
|
|
$value = is_numeric( $raw ) ? (int) $raw : 60;
|
|
|
|
printf(
|
|
'<input type="number" id="robotstxt_manager_cache_ttl_minutes" name="robotstxt_manager_cache_ttl_minutes" value="%d" class="small-text" min="1" max="1440" />',
|
|
absint( $value )
|
|
);
|
|
echo '<p class="description">' . esc_html__( 'How long the catalog response from Core is cached in a transient. Default: 60 minutes. Lower values refresh more often at the cost of more requests to Core. Maximum: 1440 (24 hours).', 'robotstxt-manager' ) . '</p>';
|
|
}
|
|
|
|
/**
|
|
* Renders the delete-on-uninstall checkbox.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function render_field_delete_on_uninstall(): void {
|
|
$value = (bool) get_option( 'robotstxt_manager_delete_data_on_uninstall', false );
|
|
echo '<label>';
|
|
printf(
|
|
'<input type="checkbox" id="robotstxt_manager_delete_data_on_uninstall" name="robotstxt_manager_delete_data_on_uninstall" value="1"%s />',
|
|
checked( $value, true, false )
|
|
);
|
|
echo ' ' . esc_html__( 'Delete all plugin data when the plugin is uninstalled.', 'robotstxt-manager' );
|
|
echo '</label>';
|
|
}
|
|
|
|
/**
|
|
* Sanitizes the API key field.
|
|
*
|
|
* Empty input preserves the existing encrypted value; non-empty input is
|
|
* encrypted before storage. The catalog transient is cleared whenever
|
|
* the key changes so the next page load fetches fresh data.
|
|
*
|
|
* @param mixed $value Raw submitted value.
|
|
*
|
|
* @return string Encrypted key, or existing value if input is empty.
|
|
*/
|
|
public function sanitize_api_key( mixed $value ): string {
|
|
$plain = sanitize_text_field( is_string( $value ) ? $value : '' );
|
|
|
|
if ( '' === $plain ) {
|
|
$raw = get_option( 'robotstxt_manager_api_key', '' );
|
|
return is_string( $raw ) ? $raw : '';
|
|
}
|
|
|
|
delete_transient( 'robotstxt_manager_catalog' );
|
|
|
|
return Robotstxt_Manager_Encryption::encrypt( $plain );
|
|
}
|
|
|
|
/**
|
|
* Sanitizes the cache TTL value.
|
|
*
|
|
* @param mixed $value Raw submitted value.
|
|
*
|
|
* @return int Clamped to 1-1440 minutes.
|
|
*/
|
|
public function sanitize_cache_ttl( mixed $value ): int {
|
|
return max( 1, min( 1440, absint( is_scalar( $value ) ? $value : 0 ) ) );
|
|
}
|
|
|
|
/**
|
|
* Handles the "Test connection" AJAX request.
|
|
*
|
|
* Reads the current Store URL + API key from options, attempts a GET
|
|
* /plugins against Core, and returns success/failure with a message.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function handle_test_connection(): void {
|
|
check_ajax_referer( 'robotstxt_manager_test_connection', 'nonce' );
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
|
|
}
|
|
|
|
$client = Robotstxt_Manager_Core_Client::from_options();
|
|
$result = $client->test_connection();
|
|
|
|
if ( $result['ok'] ) {
|
|
wp_send_json_success( array( 'message' => $result['message'] ) );
|
|
}
|
|
|
|
wp_send_json_error( array( 'message' => $result['message'] ) );
|
|
}
|
|
|
|
/**
|
|
* Handles the "Delete API key" AJAX request.
|
|
*
|
|
* Removes the encrypted API key from wp_options. The change takes effect
|
|
* immediately — subsequent catalog fetches and connection tests will fail
|
|
* until a new key is entered and saved.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function handle_delete_key(): void {
|
|
check_ajax_referer( 'robotstxt_manager_delete_key', 'nonce' );
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
|
|
}
|
|
|
|
delete_option( 'robotstxt_manager_api_key' );
|
|
delete_transient( 'robotstxt_manager_catalog' );
|
|
|
|
wp_send_json_success(
|
|
array(
|
|
'message' => __( 'API key deleted. Save changes to persist.', 'robotstxt-manager' ),
|
|
)
|
|
);
|
|
}
|
|
}
|