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( '', esc_attr( $value ) ); echo '
' . esc_html__( 'Base URL of the remote Plugins Core installation that this site will pull the plugin catalog from.', 'robotstxt-manager' ) . '
'; } /** * 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 ''; if ( $has_key ) { echo '';
if ( '' !== $last4 ) {
echo wp_kses_post(
sprintf(
/* translators: %s: last 4 characters of the stored API key. */
__( 'A key is stored (last 4 characters: %s). 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 '
' . esc_html__( 'Account-level API key from the ROBOTSTXT store (create your free account there to get one). Optional — the free catalog works without it — but required to link your subscriptions, install premium plugins, and receive their updates. Encrypted before storage.', 'robotstxt-manager' ) . '
'; } // Action buttons. echo ''; // Test connection: disabled when no key is stored. $test_disabled = $has_key ? '' : ' disabled'; printf( '', esc_attr( $test_disabled ), esc_html__( 'Test connection', 'robotstxt-manager' ) ); // Delete stored key: only shown when a key exists. if ( $has_key ) { echo ' '; } echo ''; echo '
'; } /** * 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( '', absint( $value ) ); echo '' . 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' ) . '
'; } /** * 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 ''; } /** * 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 : ''; } // Account keys are UUIDs issued by the store; reject anything that // cannot be one rather than storing a mangled key that only fails // later at connection time. if ( ! preg_match( '/^[a-f0-9][a-f0-9-]{7,126}$/i', $plain ) ) { add_settings_error( 'robotstxt_manager_api_key', 'invalid_api_key', esc_html__( 'The API key format is invalid. Copy the full key from your ROBOTSTXT account page.', 'robotstxt-manager' ) ); $raw = get_option( 'robotstxt_manager_api_key', '' ); return is_string( $raw ) ? $raw : ''; } delete_transient( 'robotstxt_manager_catalog' ); delete_transient( 'robotstxt_manager_subscriptions' ); 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' ); delete_transient( 'robotstxt_manager_subscriptions' ); wp_send_json_success( array( 'message' => __( 'API key deleted. Save changes to persist.', 'robotstxt-manager' ), ) ); } }