This commit is contained in:
Javier Casares 2026-08-18 10:48:13 +00:00
commit 5589c54b9b
13 changed files with 138 additions and 58 deletions

View file

@ -33,7 +33,8 @@ class Robotstxt_Manager_Admin {
* @return void * @return void
*/ */
public function register( Robotstxt_Manager_Loader $loader ): 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' ); $loader->add_action( 'admin_post_robotstxt_manager_refresh_catalog', $this, 'handle_refresh' );
} }
@ -43,10 +44,11 @@ class Robotstxt_Manager_Admin {
* @return void * @return void
*/ */
public function add_menu(): void { public function add_menu(): void {
$cap = is_multisite() ? 'manage_network_options' : 'manage_options';
add_menu_page( add_menu_page(
esc_html__( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ), esc_html__( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ),
esc_html__( 'ROBOTSTXT', 'robotstxt-manager' ), esc_html__( 'ROBOTSTXT', 'robotstxt-manager' ),
'manage_options', $cap,
self::PAGE_SLUG, self::PAGE_SLUG,
array( $this, 'render_page' ), array( $this, 'render_page' ),
'dashicons-screenoptions', 'dashicons-screenoptions',
@ -60,7 +62,7 @@ class Robotstxt_Manager_Admin {
* @return void * @return void
*/ */
public function render_page(): 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' ) ); 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 * @return void
*/ */
public function handle_refresh(): 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' ) ); wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) );
} }
check_admin_referer( 'robotstxt_manager_refresh_catalog' ); check_admin_referer( 'robotstxt_manager_refresh_catalog' );
$bucket = 'robotstxt_manager_refresh_' . get_current_user_id(); $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; $hits = is_numeric( $hits_raw ) ? (int) $hits_raw : 0;
if ( $hits >= 6 ) { if ( $hits >= 6 ) {
$this->redirect_refresh_error(); $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 = Robotstxt_Manager_Core_Client::from_options();
$client->clear_catalog_cache(); $client->clear_catalog_cache();
@ -175,7 +177,7 @@ class Robotstxt_Manager_Admin {
'page' => self::PAGE_SLUG, 'page' => self::PAGE_SLUG,
'refreshed' => '1', 'refreshed' => '1',
), ),
admin_url( 'admin.php' ) ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) )
); );
wp_safe_redirect( $redirect ); wp_safe_redirect( $redirect );
@ -197,7 +199,7 @@ class Robotstxt_Manager_Admin {
__( 'Too many refreshes. Please wait a minute before refreshing again.', 'robotstxt-manager' ) __( '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; exit;

View file

@ -417,7 +417,7 @@ class Robotstxt_Manager_Installer {
* @return string The sanitized plugin slug. * @return string The sanitized plugin slug.
*/ */
private function authorize( string $action ): string { 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' ) ); wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) );
} }
@ -543,7 +543,7 @@ class Robotstxt_Manager_Installer {
$headers = array(); $headers = array();
if ( $use_download_endpoint ) { 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 ) : ''; $api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : '';
if ( '' !== $api_key ) { if ( '' !== $api_key ) {
@ -686,7 +686,7 @@ class Robotstxt_Manager_Installer {
'robotstxt_manager_result' => $result, 'robotstxt_manager_result' => $result,
'robotstxt_manager_message' => rawurlencode( $message ), 'robotstxt_manager_message' => rawurlencode( $message ),
), ),
admin_url( 'admin.php' ) ( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) )
) )
); );
exit; exit;

View file

@ -41,7 +41,8 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function register( Robotstxt_Manager_Loader $loader ): 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_init', $this, 'register_settings' );
$loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_scripts' ); $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_test_connection', $this, 'handle_test_connection' );
@ -54,11 +55,12 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function add_settings_page(): void { public function add_settings_page(): void {
$cap = is_multisite() ? 'manage_network_options' : 'manage_options';
add_submenu_page( add_submenu_page(
Robotstxt_Manager_Admin::PAGE_SLUG, Robotstxt_Manager_Admin::PAGE_SLUG,
esc_html__( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ), esc_html__( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ),
esc_html__( 'Settings', 'robotstxt-manager' ), esc_html__( 'Settings', 'robotstxt-manager' ),
'manage_options', $cap,
self::PAGE_SLUG, self::PAGE_SLUG,
array( $this, 'render_page' ) array( $this, 'render_page' )
); );
@ -203,20 +205,84 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function render_page(): 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' ) ); 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'; 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. * Renders the Store URL field.
* *
* @return void * @return void
*/ */
public function render_field_store_url(): 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'; $value = is_string( $raw ) ? $raw : 'https://www.robotstxt.software';
printf( printf(
@ -234,7 +300,7 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function render_field_api_key(): 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; $has_key = is_string( $stored ) && '' !== $stored;
$last4 = ''; $last4 = '';
@ -297,7 +363,7 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function render_field_cache_ttl(): 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; $value = is_numeric( $raw ) ? (int) $raw : 60;
printf( printf(
@ -313,7 +379,7 @@ class Robotstxt_Manager_Settings {
* @return void * @return void
*/ */
public function render_field_delete_on_uninstall(): 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 '<label>'; echo '<label>';
printf( printf(
'<input type="checkbox" id="robotstxt_manager_delete_data_on_uninstall" name="robotstxt_manager_delete_data_on_uninstall" value="1"%s />', '<input type="checkbox" id="robotstxt_manager_delete_data_on_uninstall" name="robotstxt_manager_delete_data_on_uninstall" value="1"%s />',
@ -338,7 +404,7 @@ class Robotstxt_Manager_Settings {
$plain = sanitize_text_field( is_string( $value ) ? $value : '' ); $plain = sanitize_text_field( is_string( $value ) ? $value : '' );
if ( '' === $plain ) { if ( '' === $plain ) {
$raw = get_option( 'robotstxt_manager_api_key', '' ); $raw = get_site_option( 'robotstxt_manager_api_key', '' );
return is_string( $raw ) ? $raw : ''; return is_string( $raw ) ? $raw : '';
} }
@ -360,12 +426,12 @@ class Robotstxt_Manager_Settings {
esc_html__( 'The API key format is invalid. Copy the full key from your ROBOTSTXT account page.', 'robotstxt-manager' ) 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', '' ); $raw = get_site_option( 'robotstxt_manager_api_key', '' );
return is_string( $raw ) ? $raw : ''; return is_string( $raw ) ? $raw : '';
} }
delete_transient( 'robotstxt_manager_catalog' ); delete_site_transient( 'robotstxt_manager_catalog' );
delete_transient( 'robotstxt_manager_subscriptions' ); delete_site_transient( 'robotstxt_manager_subscriptions' );
return Robotstxt_Manager_Encryption::encrypt( $plain ); return Robotstxt_Manager_Encryption::encrypt( $plain );
} }
@ -392,7 +458,7 @@ class Robotstxt_Manager_Settings {
public function handle_test_connection(): void { public function handle_test_connection(): void {
check_ajax_referer( 'robotstxt_manager_test_connection', 'nonce' ); check_ajax_referer( 'robotstxt_manager_test_connection', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) ); wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
} }
@ -405,7 +471,7 @@ class Robotstxt_Manager_Settings {
} }
} }
$store_url = get_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' ); $store_url = get_site_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' );
$store_url = is_string( $store_url ) ? $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. // Use input key if provided, otherwise fall back to saved (decrypted) key.
@ -436,13 +502,13 @@ class Robotstxt_Manager_Settings {
public function handle_delete_key(): void { public function handle_delete_key(): void {
check_ajax_referer( 'robotstxt_manager_delete_key', 'nonce' ); check_ajax_referer( 'robotstxt_manager_delete_key', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) ); wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
} }
delete_option( 'robotstxt_manager_api_key' ); delete_site_option( 'robotstxt_manager_api_key' );
delete_transient( 'robotstxt_manager_catalog' ); delete_site_transient( 'robotstxt_manager_catalog' );
delete_transient( 'robotstxt_manager_subscriptions' ); delete_site_transient( 'robotstxt_manager_subscriptions' );
wp_send_json_success( wp_send_json_success(
array( array(

View file

@ -107,7 +107,7 @@ $compat_warnings = 0;
sprintf( sprintf(
/* translators: %s: settings URL. */ /* translators: %s: settings URL. */
__( 'ROBOTSTXT Manager is not configured yet. <a href="%s">Set the Store URL and API key</a> to see your plugin catalog.', 'robotstxt-manager' ), __( 'ROBOTSTXT Manager is not configured yet. <a href="%s">Set the Store URL and API key</a> to see your plugin catalog.', 'robotstxt-manager' ),
esc_url( admin_url( 'admin.php?page=' . Robotstxt_Manager_Settings::PAGE_SLUG ) ) esc_url( ( is_multisite() ? network_admin_url( 'admin.php?page=' . Robotstxt_Manager_Settings::PAGE_SLUG ) : admin_url( 'admin.php?page=' . Robotstxt_Manager_Settings::PAGE_SLUG ) ) )
) )
); );
?> ?>

View file

@ -12,9 +12,9 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ); ?></h1> <h1><?php esc_html_e( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ); ?></h1>
<?php settings_errors(); ?> <?php settings_errors(); ?>
<form method="post" action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>"> <form method="post" action="">
<?php <?php
settings_fields( Robotstxt_Manager_Settings::OPTION_GROUP ); wp_nonce_field( 'robotstxt_manager_settings_group', 'robotstxt_manager_settings_group_nonce' );
do_settings_sections( Robotstxt_Manager_Settings::PAGE_SLUG ); do_settings_sections( Robotstxt_Manager_Settings::PAGE_SLUG );
submit_button(); submit_button();
?> ?>

View file

@ -1,9 +1,13 @@
== Changelog == == Changelog ==
= 1.5.0 = = 1.6.0 =
_Release date: 2026-08-18_ _Release date: 2026-08-18_
**Added**
* WordPress Multisite compatibility: `Network: true` header forces network-wide activation. Menus appear in the network admin on Multisite, in the regular admin on single-site. All options and transients use network-level storage (`get_site_option` / `get_site_transient`). Settings form handles submission manually (the Settings API `options.php` does not handle network options).
**Fixed** **Fixed**
* API key not saving when the browser auto-fills the password field with the stored encrypted value — the sanitizer now detects already-encrypted input (`v2:` prefix) and returns it as-is instead of failing UUID validation. * API key not saving when the browser auto-fills the password field with the stored encrypted value — the sanitizer now detects already-encrypted input (`v2:` prefix) and returns it as-is instead of failing UUID validation.
@ -21,7 +25,7 @@ _Release date: 2026-08-18_
* PHPStan level 9 + wp-compat: pass * PHPStan level 9 + wp-compat: pass
* PHPUnit: 146 tests, 385 assertions * PHPUnit: 146 tests, 385 assertions
= 1.4.1 = = 1.5.0 =
_Release date: 2026-08-17_ _Release date: 2026-08-17_

View file

@ -34,7 +34,7 @@ class Robotstxt_Manager_Activator {
* @return void * @return void
*/ */
public static function deactivate(): void { public static function deactivate(): void {
delete_transient( 'robotstxt_manager_catalog' ); delete_site_transient( 'robotstxt_manager_catalog' );
} }
/** /**
@ -43,12 +43,12 @@ class Robotstxt_Manager_Activator {
* @return void * @return void
*/ */
private static function ensure_defaults(): void { private static function ensure_defaults(): void {
if ( '' === get_option( 'robotstxt_manager_store_url', '' ) ) { if ( '' === get_site_option( 'robotstxt_manager_store_url', '' ) ) {
update_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' ); update_site_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' );
} }
if ( '' === get_option( 'robotstxt_manager_cache_ttl_minutes', '' ) ) { if ( '' === get_site_option( 'robotstxt_manager_cache_ttl_minutes', '' ) ) {
update_option( 'robotstxt_manager_cache_ttl_minutes', 60 ); update_site_option( 'robotstxt_manager_cache_ttl_minutes', 60 );
} }
} }
} }

View file

@ -79,8 +79,8 @@ class Robotstxt_Manager_Core_Client {
* @return self * @return self
*/ */
public static function from_options(): self { public static function from_options(): self {
$store_url_raw = get_option( 'robotstxt_manager_store_url', '' ); $store_url_raw = get_site_option( 'robotstxt_manager_store_url', '' );
$api_key_raw = get_option( 'robotstxt_manager_api_key', '' ); $api_key_raw = get_site_option( 'robotstxt_manager_api_key', '' );
$store_url = is_string( $store_url_raw ) ? $store_url_raw : ''; $store_url = is_string( $store_url_raw ) ? $store_url_raw : '';
$api_key = is_string( $api_key_raw ) $api_key = is_string( $api_key_raw )
@ -197,7 +197,7 @@ class Robotstxt_Manager_Core_Client {
} }
$cache_key = 'robotstxt_manager_catalog'; $cache_key = 'robotstxt_manager_catalog';
$cached = get_transient( $cache_key ); $cached = get_site_transient( $cache_key );
if ( is_array( $cached ) ) { if ( is_array( $cached ) ) {
$typed = array(); $typed = array();
@ -248,7 +248,7 @@ class Robotstxt_Manager_Core_Client {
} }
$ttl = $this->get_catalog_ttl(); $ttl = $this->get_catalog_ttl();
set_transient( $cache_key, $catalog, $ttl ); set_site_transient( $cache_key, $catalog, $ttl );
return $catalog; return $catalog;
} }
@ -260,7 +260,7 @@ class Robotstxt_Manager_Core_Client {
* @return void * @return void
*/ */
public function clear_catalog_cache(): void { public function clear_catalog_cache(): void {
delete_transient( 'robotstxt_manager_catalog' ); delete_site_transient( 'robotstxt_manager_catalog' );
} }
/** /**
@ -269,7 +269,7 @@ class Robotstxt_Manager_Core_Client {
* @return int * @return int
*/ */
private function get_catalog_ttl(): int { private function get_catalog_ttl(): int {
$raw = get_option( 'robotstxt_manager_cache_ttl_minutes', 60 ); $raw = get_site_option( 'robotstxt_manager_cache_ttl_minutes', 60 );
$min = is_numeric( $raw ) ? (int) $raw : 60; $min = is_numeric( $raw ) ? (int) $raw : 60;
if ( $min < 1 ) { if ( $min < 1 ) {
@ -316,7 +316,7 @@ class Robotstxt_Manager_Core_Client {
} }
$cache_key = 'robotstxt_manager_subscriptions'; $cache_key = 'robotstxt_manager_subscriptions';
$cached = get_transient( $cache_key ); $cached = get_site_transient( $cache_key );
if ( is_array( $cached ) ) { if ( is_array( $cached ) ) {
$typed = array(); $typed = array();
@ -363,7 +363,7 @@ class Robotstxt_Manager_Core_Client {
); );
} }
set_transient( $cache_key, $rows, HOUR_IN_SECONDS ); set_site_transient( $cache_key, $rows, HOUR_IN_SECONDS );
return $rows; return $rows;
} }
@ -413,6 +413,6 @@ class Robotstxt_Manager_Core_Client {
* @return void * @return void
*/ */
public function clear_subscriptions_cache(): void { public function clear_subscriptions_cache(): void {
delete_transient( 'robotstxt_manager_subscriptions' ); delete_site_transient( 'robotstxt_manager_subscriptions' );
} }
} }

View file

@ -273,7 +273,7 @@ class Robotstxt_Manager_Updater {
* @return bool True when a usable key exists. * @return bool True when a usable key exists.
*/ */
private function has_api_key(): bool { private function has_api_key(): bool {
$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 ) : ''; $api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : '';
return '' !== $api_key; return '' !== $api_key;
@ -308,7 +308,7 @@ class Robotstxt_Manager_Updater {
$args['token'] = $token; $args['token'] = $token;
} else { } else {
// Fallback (older Core): the API key itself. // Fallback (older Core): the API key itself.
$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 ) : ''; $api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : '';
if ( '' !== $api_key ) { if ( '' !== $api_key ) {

View file

@ -2,7 +2,7 @@
# This file is distributed under the GPL-3.0-or-later. # This file is distributed under the GPL-3.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Manager (by ROBOTSTXT) 1.5.0\n" "Project-Id-Version: Manager (by ROBOTSTXT) 1.6.0\n"
"Report-Msgid-Bugs-To: https://www.robotstxt.software/plugins/robotstxt-manager/\n" "Report-Msgid-Bugs-To: https://www.robotstxt.software/plugins/robotstxt-manager/\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -3,9 +3,9 @@ Contributors: robotstxt, javiercasares
Tags: dashboard, catalog, updates, subscriptions, management Tags: dashboard, catalog, updates, subscriptions, management
Requires at least: 4.4 Requires at least: 4.4
Tested up to: 7.1 Tested up to: 7.1
Stable tag: 1.5.0 Stable tag: 1.6.0
Requires PHP: 8.0 Requires PHP: 8.0
Version: 1.5.0 Version: 1.6.0
License: GPL-3.0-or-later License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -94,16 +94,23 @@ 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 Only the 3 last versions. The full changelog will be at changelog.txt
= 1.5.0 = = 1.6.0 =
_Release date: 2026-08-18_ _Release date: 2026-08-18_
* Added: WordPress Multisite compatibility — `Network: true` header forces network-wide activation; menus appear in the network admin on Multisite and in the regular admin on single-site. All options and transients use network-level storage (`get_site_option` / `get_site_transient`).
* Fixed: API key not saving when the browser auto-fills the password field with the stored encrypted value. * 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" 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. * 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: Validation errors now display on the settings page (missing `settings_errors()` call).
* Added: Registration link in the API key field description. * Added: Registration link in the API key field description.
= 1.5.0 =
_Release date: 2026-08-18_
* Fixed: API key saving, test connection, settings error display, registration link.
= 1.4.1 = = 1.4.1 =
_Release date: 2026-08-17_ _Release date: 2026-08-17_

View file

@ -3,7 +3,7 @@
* Plugin Name: Manager (by ROBOTSTXT) * Plugin Name: Manager (by ROBOTSTXT)
* Plugin URI: https://www.robotstxt.software/plugins/robotstxt-manager/ * 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. * 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.5.0 * Version: 1.6.0
* Requires at least: 4.4 * Requires at least: 4.4
* Requires PHP: 8.0 * Requires PHP: 8.0
* Update URI: https://www.robotstxt.software/plugins/robotstxt-manager/ * Update URI: https://www.robotstxt.software/plugins/robotstxt-manager/
@ -13,6 +13,7 @@
* License URI: https://www.gnu.org/licenses/gpl-3.0.html * License URI: https://www.gnu.org/licenses/gpl-3.0.html
* Text Domain: robotstxt-manager * Text Domain: robotstxt-manager
* Domain Path: /languages * Domain Path: /languages
* Network: true
* *
* @package Robotstxt_Manager * @package Robotstxt_Manager
*/ */
@ -22,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
/** Plugin version. */ /** Plugin version. */
define( 'ROBOTSTXT_MANAGER_VERSION', '1.5.0' ); define( 'ROBOTSTXT_MANAGER_VERSION', '1.6.0' );
/** Absolute path to the plugin directory, with trailing slash. */ /** Absolute path to the plugin directory, with trailing slash. */
define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) ); define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) );

View file

@ -9,7 +9,7 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit; exit;
} }
if ( ! get_option( 'robotstxt_manager_delete_data_on_uninstall', false ) ) { if ( ! get_site_option( 'robotstxt_manager_delete_data_on_uninstall', false ) ) {
return; return;
} }
@ -21,10 +21,10 @@ $option_keys = array(
); );
foreach ( $option_keys as $key ) { foreach ( $option_keys as $key ) {
delete_option( $key ); delete_site_option( $key );
} }
delete_transient( 'robotstxt_manager_catalog' ); delete_site_transient( 'robotstxt_manager_catalog' );
// Purge the WordPress update transient: premium entries carry the API key // Purge the WordPress update transient: premium entries carry the API key
// in their package URL. WordPress rebuilds it on the next update check. // in their package URL. WordPress rebuilds it on the next update check.