v0.1.0
This commit is contained in:
commit
bc1cb5e00a
32 changed files with 4254 additions and 0 deletions
402
admin/class-robotstxt-manager-settings.php
Normal file
402
admin/class-robotstxt-manager-settings.php
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
<?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://plugins.robotstxt.es',
|
||||
)
|
||||
);
|
||||
|
||||
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://plugins.robotstxt.es' );
|
||||
$value = is_string( $raw ) ? $raw : 'https://plugins.robotstxt.es';
|
||||
|
||||
printf(
|
||||
'<input type="url" id="robotstxt_manager_store_url" name="robotstxt_manager_store_url" value="%s" class="regular-text" maxlength="500" placeholder="https://plugins.robotstxt.es" />',
|
||||
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' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue