From b2849880ec70d717b0d4143152fb860feeee0a38 Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Tue, 18 Aug 2026 10:47:06 +0000 Subject: [PATCH 1/7] v1.5.0 --- admin/class-robotstxt-manager-settings.php | 39 ++++++++++++++++++- admin/js/robotstxt-manager-settings.js | 3 ++ admin/views/page-settings.php | 1 + changelog.txt | 21 ++++++++++ .../class-robotstxt-manager-core-client.php | 11 +++++- languages/robotstxt-manager.pot | 8 +++- readme.txt | 20 ++++++---- robotstxt-manager.php | 4 +- 8 files changed, 91 insertions(+), 16 deletions(-) diff --git a/admin/class-robotstxt-manager-settings.php b/admin/class-robotstxt-manager-settings.php index cddc28f..2cbd759 100644 --- a/admin/class-robotstxt-manager-settings.php +++ b/admin/class-robotstxt-manager-settings.php @@ -263,7 +263,16 @@ class Robotstxt_Manager_Settings { } echo '

'; } else { - 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' ) . '

'; + printf( + '

%s

', + wp_kses_post( + sprintf( + /* translators: %s: Registration URL. */ + __( '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' ), + esc_url( 'https://www.robotstxt.software/wp-login.php?action=register' ) + ) + ) + ); } // Action buttons. @@ -333,6 +342,14 @@ class Robotstxt_Manager_Settings { return is_string( $raw ) ? $raw : ''; } + // If the input is already encrypted (v2: prefix), it means the browser + // auto-filled the password field with the stored encrypted value. + // Return it as-is (already encrypted) rather than re-encrypting or + // trying to read the option (which may not be saved yet in the WP flow). + if ( str_starts_with( $plain, 'v2:' ) ) { + return $plain; + } + // 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. @@ -379,7 +396,25 @@ class Robotstxt_Manager_Settings { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) ); } - $client = Robotstxt_Manager_Core_Client::from_options(); + // Allow testing a key from the form field (not yet saved) by passing it in the request. + $input_key = ''; + if ( isset( $_POST['robotstxt_manager_api_key'] ) ) { + $unslashed = wp_unslash( $_POST['robotstxt_manager_api_key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below. + if ( is_string( $unslashed ) ) { + $input_key = sanitize_text_field( $unslashed ); + } + } + + $store_url = get_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' ); + $store_url = is_string( $store_url ) ? $store_url : 'https://www.robotstxt.software'; + + // Use input key if provided, otherwise fall back to saved (decrypted) key. + if ( '' !== $input_key ) { + $client = new Robotstxt_Manager_Core_Client( $store_url, $input_key ); + } else { + $client = Robotstxt_Manager_Core_Client::from_options(); + } + $result = $client->test_connection(); if ( $result['ok'] ) { diff --git a/admin/js/robotstxt-manager-settings.js b/admin/js/robotstxt-manager-settings.js index c0f465a..f11d499 100644 --- a/admin/js/robotstxt-manager-settings.js +++ b/admin/js/robotstxt-manager-settings.js @@ -20,11 +20,14 @@ $btn.prop( 'disabled', true ).text( RobotstxtManagerSettings.i18n.testing ); $result.text( '' ).css( 'color', '' ); + var apiKey = $( '#robotstxt_manager_api_key' ).val(); + $.post( RobotstxtManagerSettings.ajaxUrl, { action: 'robotstxt_manager_test_connection', nonce: RobotstxtManagerSettings.nonce, + robotstxt_manager_api_key: apiKey, }, function ( response ) { if ( response.success ) { diff --git a/admin/views/page-settings.php b/admin/views/page-settings.php index 89b5d4a..83cd17d 100644 --- a/admin/views/page-settings.php +++ b/admin/views/page-settings.php @@ -11,6 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { ?>

+
get( '/plugins' ); + $response = $this->get( '/me/subscriptions' ); if ( is_wp_error( $response ) ) { return array( @@ -161,6 +161,13 @@ class Robotstxt_Manager_Core_Client { $code = (int) wp_remote_retrieve_response_code( $response ); + if ( 401 === $code ) { + return array( + 'ok' => false, + 'message' => __( 'Invalid API key. Please check your key and try again.', 'robotstxt-manager' ), + ); + } + if ( 200 !== $code ) { return array( 'ok' => false, @@ -175,7 +182,7 @@ class Robotstxt_Manager_Core_Client { return array( 'ok' => true, 'message' => __( 'Connected.', 'robotstxt-manager' ), - 'catalog_count' => $count, + 'subscriptions' => $count, ); } diff --git a/languages/robotstxt-manager.pot b/languages/robotstxt-manager.pot index e0350b5..cae3906 100644 --- a/languages/robotstxt-manager.pot +++ b/languages/robotstxt-manager.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-3.0-or-later. msgid "" msgstr "" -"Project-Id-Version: Manager (by ROBOTSTXT) 1.4.1\n" +"Project-Id-Version: Manager (by ROBOTSTXT) 1.5.0\n" "Report-Msgid-Bugs-To: https://www.robotstxt.software/plugins/robotstxt-manager/\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -259,7 +259,7 @@ msgid "A key is stored but could not be decoded. You can replace it by entering msgstr "" #: admin/class-robotstxt-manager-settings.php:266 -msgid "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." +msgid "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." msgstr "" #: admin/class-robotstxt-manager-settings.php:298 @@ -475,6 +475,10 @@ msgstr "" msgid "The store responded with HTTP %d. Check the Store URL and API key." msgstr "" +#: includes/class-robotstxt-manager-core-client.php:164 +msgid "Invalid API key. Please check your key and try again." +msgstr "" + #: includes/class-robotstxt-manager-core-client.php:177 msgid "Connected." msgstr "" diff --git a/readme.txt b/readme.txt index b2f0888..1c495ba 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: robotstxt, javiercasares Tags: dashboard, catalog, updates, subscriptions, management Requires at least: 4.4 Tested up to: 7.1 -Stable tag: 1.4.1 +Stable tag: 1.5.0 Requires PHP: 8.0 -Version: 1.4.1 +Version: 1.5.0 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt @@ -94,6 +94,16 @@ Encrypted at rest using AES-256-CBC with a key derived from your site's WordPres Only the 3 last versions. The full changelog will be at changelog.txt += 1.5.0 = + +_Release date: 2026-08-18_ + +* Fixed: API key not saving when the browser auto-fills the password field with the stored encrypted value. +* Fixed: "Test connection" now validates the API key against an authenticated Core endpoint (`/me/subscriptions`) instead of the public catalog — invalid keys are correctly rejected. +* Fixed: "Test connection" reads the key from the form field, so a key can be tested before saving. +* Added: Validation errors now display on the settings page (missing `settings_errors()` call). +* Added: Registration link in the API key field description. + = 1.4.1 = _Release date: 2026-08-17_ @@ -107,12 +117,6 @@ _Release date: 2026-08-17_ * Premium update URLs carry a short-lived download token (Core 1.9.0+) instead of the API key; automatic fallback on older Core. -= 1.3.1 = - -_Release date: 2026-08-17_ - -* Descriptions render in the admin's language (Core 1.8.0+ `description_translations`), English fallback. - = Previous versions = If you want to see the full changelog, visit the [plugin page](https://www.robotstxt.software/plugins/robotstxt-manager/). diff --git a/robotstxt-manager.php b/robotstxt-manager.php index dfa157b..846e580 100644 --- a/robotstxt-manager.php +++ b/robotstxt-manager.php @@ -3,7 +3,7 @@ * Plugin Name: Manager (by ROBOTSTXT) * Plugin URI: https://www.robotstxt.software/plugins/robotstxt-manager/ * Description: Client-side dashboard for the ROBOTSTXT plugin ecosystem. Lists the catalog from a remote Plugins Core install, resolves local install/update state, and installs, activates, and updates plugins directly from the store. - * Version: 1.4.1 + * Version: 1.5.0 * Requires at least: 4.4 * Requires PHP: 8.0 * Update URI: https://www.robotstxt.software/plugins/robotstxt-manager/ @@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** Plugin version. */ -define( 'ROBOTSTXT_MANAGER_VERSION', '1.4.1' ); +define( 'ROBOTSTXT_MANAGER_VERSION', '1.5.0' ); /** Absolute path to the plugin directory, with trailing slash. */ define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) ); From 5589c54b9bfaa2fd8ceaed857f92b47c40d2965e Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Tue, 18 Aug 2026 10:48:13 +0000 Subject: [PATCH 2/7] v1.6.0 --- admin/class-robotstxt-manager-admin.php | 18 ++-- admin/class-robotstxt-manager-installer.php | 6 +- admin/class-robotstxt-manager-settings.php | 100 +++++++++++++++--- admin/views/page-catalog.php | 2 +- admin/views/page-settings.php | 4 +- changelog.txt | 8 +- .../class-robotstxt-manager-activator.php | 10 +- .../class-robotstxt-manager-core-client.php | 18 ++-- includes/class-robotstxt-manager-updater.php | 4 +- languages/robotstxt-manager.pot | 2 +- readme.txt | 13 ++- robotstxt-manager.php | 5 +- uninstall.php | 6 +- 13 files changed, 138 insertions(+), 58 deletions(-) diff --git a/admin/class-robotstxt-manager-admin.php b/admin/class-robotstxt-manager-admin.php index 1921cc6..20b8b41 100644 --- a/admin/class-robotstxt-manager-admin.php +++ b/admin/class-robotstxt-manager-admin.php @@ -33,7 +33,8 @@ class Robotstxt_Manager_Admin { * @return void */ public function register( Robotstxt_Manager_Loader $loader ): void { - $loader->add_action( 'admin_menu', $this, 'add_menu' ); + $menu_hook = is_multisite() ? 'network_admin_menu' : 'admin_menu'; + $loader->add_action( $menu_hook, $this, 'add_menu' ); $loader->add_action( 'admin_post_robotstxt_manager_refresh_catalog', $this, 'handle_refresh' ); } @@ -43,10 +44,11 @@ class Robotstxt_Manager_Admin { * @return void */ public function add_menu(): void { + $cap = is_multisite() ? 'manage_network_options' : 'manage_options'; add_menu_page( esc_html__( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ), esc_html__( 'ROBOTSTXT', 'robotstxt-manager' ), - 'manage_options', + $cap, self::PAGE_SLUG, array( $this, 'render_page' ), 'dashicons-screenoptions', @@ -60,7 +62,7 @@ class Robotstxt_Manager_Admin { * @return void */ public function render_page(): void { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) ); } @@ -149,21 +151,21 @@ class Robotstxt_Manager_Admin { * @return void */ public function handle_refresh(): void { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) ); } check_admin_referer( 'robotstxt_manager_refresh_catalog' ); $bucket = 'robotstxt_manager_refresh_' . get_current_user_id(); - $hits_raw = get_transient( $bucket ); + $hits_raw = get_site_transient( $bucket ); $hits = is_numeric( $hits_raw ) ? (int) $hits_raw : 0; if ( $hits >= 6 ) { $this->redirect_refresh_error(); } - set_transient( $bucket, $hits + 1, MINUTE_IN_SECONDS ); + set_site_transient( $bucket, $hits + 1, MINUTE_IN_SECONDS ); $client = Robotstxt_Manager_Core_Client::from_options(); $client->clear_catalog_cache(); @@ -175,7 +177,7 @@ class Robotstxt_Manager_Admin { 'page' => self::PAGE_SLUG, 'refreshed' => '1', ), - admin_url( 'admin.php' ) + ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) ) ); wp_safe_redirect( $redirect ); @@ -197,7 +199,7 @@ class Robotstxt_Manager_Admin { __( 'Too many refreshes. Please wait a minute before refreshing again.', 'robotstxt-manager' ) ), ), - admin_url( 'admin.php' ) + ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) ) ) ); exit; diff --git a/admin/class-robotstxt-manager-installer.php b/admin/class-robotstxt-manager-installer.php index 2714e78..28046a0 100644 --- a/admin/class-robotstxt-manager-installer.php +++ b/admin/class-robotstxt-manager-installer.php @@ -417,7 +417,7 @@ class Robotstxt_Manager_Installer { * @return string The sanitized plugin slug. */ private function authorize( string $action ): string { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) ); } @@ -543,7 +543,7 @@ class Robotstxt_Manager_Installer { $headers = array(); if ( $use_download_endpoint ) { - $api_key_raw = get_option( 'robotstxt_manager_api_key', '' ); + $api_key_raw = get_site_option( 'robotstxt_manager_api_key', '' ); $api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : ''; if ( '' !== $api_key ) { @@ -686,7 +686,7 @@ class Robotstxt_Manager_Installer { 'robotstxt_manager_result' => $result, 'robotstxt_manager_message' => rawurlencode( $message ), ), - admin_url( 'admin.php' ) + ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) ) ) ); exit; diff --git a/admin/class-robotstxt-manager-settings.php b/admin/class-robotstxt-manager-settings.php index 2cbd759..d1fcf12 100644 --- a/admin/class-robotstxt-manager-settings.php +++ b/admin/class-robotstxt-manager-settings.php @@ -41,7 +41,8 @@ class Robotstxt_Manager_Settings { * @return void */ public function register( Robotstxt_Manager_Loader $loader ): void { - $loader->add_action( 'admin_menu', $this, 'add_settings_page' ); + $menu_hook = is_multisite() ? 'network_admin_menu' : 'admin_menu'; + $loader->add_action( $menu_hook, $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' ); @@ -54,11 +55,12 @@ class Robotstxt_Manager_Settings { * @return void */ public function add_settings_page(): void { + $cap = is_multisite() ? 'manage_network_options' : 'manage_options'; add_submenu_page( Robotstxt_Manager_Admin::PAGE_SLUG, esc_html__( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ), esc_html__( 'Settings', 'robotstxt-manager' ), - 'manage_options', + $cap, self::PAGE_SLUG, array( $this, 'render_page' ) ); @@ -203,20 +205,84 @@ class Robotstxt_Manager_Settings { * @return void */ public function render_page(): void { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) ); } + $this->handle_form_submission(); + require_once ROBOTSTXT_MANAGER_DIR . 'admin/views/page-settings.php'; } + /** + * Handles manual form submission for network settings. + * + * The WordPress Settings API (options.php) does not handle network + * options, so the network settings page must process its own form. + * + * @return void + */ + private function handle_form_submission(): void { + if ( ! isset( $_POST['robotstxt_manager_settings_group_nonce'] ) ) { + return; + } + + check_admin_referer( 'robotstxt_manager_settings_group', 'robotstxt_manager_settings_group_nonce' ); + + if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to manage settings.', 'robotstxt-manager' ) ); + } + + // Store URL. + $store_url = ''; + if ( isset( $_POST['robotstxt_manager_store_url'] ) ) { + $raw = wp_unslash( $_POST['robotstxt_manager_store_url'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below. + $store_url = is_string( $raw ) ? esc_url_raw( $raw ) : ''; + } + update_site_option( 'robotstxt_manager_store_url', $store_url ); + + // API key. + $api_key = $this->sanitize_api_key( '' ); + if ( isset( $_POST['robotstxt_manager_api_key'] ) ) { + $raw = wp_unslash( $_POST['robotstxt_manager_api_key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized by sanitize_api_key(). + if ( is_string( $raw ) ) { + $api_key = $this->sanitize_api_key( $raw ); + } + } + update_site_option( 'robotstxt_manager_api_key', $api_key ); + + // Cache TTL. + $cache_ttl = 60; + if ( isset( $_POST['robotstxt_manager_cache_ttl_minutes'] ) ) { + $raw = wp_unslash( $_POST['robotstxt_manager_cache_ttl_minutes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized by sanitize_cache_ttl(). + $cache_ttl = $this->sanitize_cache_ttl( $raw ); + } + update_site_option( 'robotstxt_manager_cache_ttl_minutes', $cache_ttl ); + + // Delete on uninstall. + $delete_on_uninstall = isset( $_POST['robotstxt_manager_delete_data_on_uninstall'] ) ? true : false; + update_site_option( 'robotstxt_manager_delete_data_on_uninstall', $delete_on_uninstall ); + + // Redirect with success flag. + $goback = add_query_arg( + array( + 'page' => self::PAGE_SLUG, + 'settings-updated' => 'true', + ), + ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) ) + ); + + wp_safe_redirect( $goback ); + exit; + } + /** * 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' ); + $raw = get_site_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' ); $value = is_string( $raw ) ? $raw : 'https://www.robotstxt.software'; printf( @@ -234,7 +300,7 @@ class Robotstxt_Manager_Settings { * @return void */ public function render_field_api_key(): void { - $stored = get_option( 'robotstxt_manager_api_key', '' ); + $stored = get_site_option( 'robotstxt_manager_api_key', '' ); $has_key = is_string( $stored ) && '' !== $stored; $last4 = ''; @@ -297,7 +363,7 @@ class Robotstxt_Manager_Settings { * @return void */ public function render_field_cache_ttl(): void { - $raw = get_option( 'robotstxt_manager_cache_ttl_minutes', 60 ); + $raw = get_site_option( 'robotstxt_manager_cache_ttl_minutes', 60 ); $value = is_numeric( $raw ) ? (int) $raw : 60; printf( @@ -313,7 +379,7 @@ class Robotstxt_Manager_Settings { * @return void */ public function render_field_delete_on_uninstall(): void { - $value = (bool) get_option( 'robotstxt_manager_delete_data_on_uninstall', false ); + $value = (bool) get_site_option( 'robotstxt_manager_delete_data_on_uninstall', false ); echo '