diff --git a/admin/class-robotstxt-manager-settings.php b/admin/class-robotstxt-manager-settings.php index 1c2d7ac..6e54620 100644 --- a/admin/class-robotstxt-manager-settings.php +++ b/admin/class-robotstxt-manager-settings.php @@ -333,6 +333,20 @@ class Robotstxt_Manager_Settings { 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' ); return Robotstxt_Manager_Encryption::encrypt( $plain ); diff --git a/changelog.txt b/changelog.txt index e14081a..24095ca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,30 @@ == Changelog == += 0.5.1 = + +_Release date: 2026-08-15_ + +**Highlights** + +* Stabilization release after a full code + security audit (fresh-context review per AGENTS-deploy.md) and full-range compatibility scans (PHPCompatibility 5.6–8.5, wp-compat laddering). Declared requirements now match the real floors: **WordPress 4.4+** (was 4.7) and **PHP 8.0+** (was 7.4 — the updater's `mixed` type hints and `str_contains()` require 8.0; previously under-declared, which would have been a fatal on 7.4). + +**Fixed** + +* `test_connection()` reported "Connected." on non-200 responses and `get_catalog()` cached auth failures as an empty catalog for the full TTL. Both now check the HTTP status; non-200 responses return an error/empty and are never cached. +* `site_domain()` used `ltrim( $host, 'www.' )`, which strips a character set and mangles hosts starting with `w` (e.g. `webdev.example.com` → `ebdev…`, breaking premium package URLs). Now strips only the literal `www.` prefix. +* Native-update integration no longer injects premium `response` entries when no decryptable API key exists (the native updater would download into a 403). +* Opt-in uninstall: also deletes the `update_plugins` site transient (premium entries embed the API key in the package URL — the plaintext copy must not outlive the plugin) and stops deleting a phantom `robotstxt_manager_db_version` option nothing ever wrote. + +**Changed** + +* API-key setting validates the format (UUID-like, 8–127 chars) before storing, rejecting mangled input at save time instead of failing later at connection time. +* Plugin version 0.5.0 → 0.5.1. No database schema changes (no custom tables). + +**Compatibility** + +* WordPress: 4.4 - 7.1 (scan-verified: wp-compat clean from 4.4) +* PHP: 8.0 - 8.5 (scan-verified: PHPCompatibility + manual feature audit) + = 0.5.0 = _Release date: 2026-08-14_ diff --git a/includes/class-robotstxt-manager-core-client.php b/includes/class-robotstxt-manager-core-client.php index 6fc3af6..ce39709 100644 --- a/includes/class-robotstxt-manager-core-client.php +++ b/includes/class-robotstxt-manager-core-client.php @@ -146,8 +146,18 @@ class Robotstxt_Manager_Core_Client { ); } - $body = wp_remote_retrieve_body( $response ); - $count = is_array( json_decode( $body, true ) ) ? count( json_decode( $body, true ) ) : 0; + $code = (int) wp_remote_retrieve_response_code( $response ); + + if ( 200 !== $code ) { + return array( + 'ok' => false, + /* translators: %d: HTTP status code. */ + 'message' => sprintf( __( 'The store responded with HTTP %d. Check the Store URL and API key.', 'robotstxt-manager' ), $code ), + ); + } + + $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); + $count = is_array( $decoded ) ? count( $decoded ) : 0; return array( 'ok' => true, @@ -191,6 +201,12 @@ class Robotstxt_Manager_Core_Client { return array(); } + // A non-200 response (auth failure, outage) must not be cached as an + // "empty catalog" for the full TTL — return nothing and retry next time. + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return array(); + } + $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); diff --git a/includes/class-robotstxt-manager-updater.php b/includes/class-robotstxt-manager-updater.php index bd98198..ca8a746 100644 --- a/includes/class-robotstxt-manager-updater.php +++ b/includes/class-robotstxt-manager-updater.php @@ -92,6 +92,13 @@ class Robotstxt_Manager_Updater { } if ( version_compare( $version, $new_version, '<' ) ) { + // Premium updates need the account key in the package URL; + // without a usable key the native updater would only hit a + // 403, so skip injecting the entry. + if ( 'premium' === $entry['type'] && ! $this->has_api_key() ) { + continue; + } + if ( property_exists( $transient, 'response' ) && is_array( $transient->response ) ) { $transient->response[ $plugin_file ] = $this->build_update_object( $slug, (string) $plugin_file, $entry ); } @@ -260,6 +267,18 @@ class Robotstxt_Manager_Updater { return (object) $data; } + /** + * Returns whether a decryptable account API key is configured. + * + * @return bool True when a usable key exists. + */ + private function has_api_key(): bool { + $api_key_raw = get_option( 'robotstxt_manager_api_key', '' ); + $api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : ''; + + return '' !== $api_key; + } + /** * Builds the download package URL for the native upgrader. * @@ -298,9 +317,10 @@ class Robotstxt_Manager_Updater { * @return string Domain (e.g. 'example.com'). */ private function site_domain(): string { - $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); + $host = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ); - return strtolower( ltrim( $host, 'www.' ) ); + // Strip the literal "www." prefix (ltrim would eat any leading w/.). + return (string) preg_replace( '/^www\./', '', $host ); } /** diff --git a/readme.txt b/readme.txt index 555ab09..2766b7c 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === Manager (by ROBOTSTXT) === Contributors: javiercasares, robotstxt Tags: dashboard, catalog, updates, subscriptions, management -Requires at least: 4.7 +Requires at least: 4.4 Tested up to: 7.1 -Stable tag: 0.5.0 -Requires PHP: 7.4 -Version: 0.5.0 +Stable tag: 0.5.1 +Requires PHP: 8.0 +Version: 0.5.1 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt @@ -72,7 +72,7 @@ No. This plugin is intentionally not compatible with WordPress Multisite, matchi = What PHP version is required? = -PHP 8.4 or higher. +PHP 8.0 or higher. = Do I need Plugins Core installed on my site? = @@ -84,11 +84,22 @@ Encrypted at rest using AES-256-CBC with a key derived from your site's WordPres == Compatibility == -* WordPress: 4.7 - 7.1 -* PHP: 7.4 - 8.5 +* WordPress: 4.4 - 7.1 +* PHP: 8.0 - 8.5 == Changelog == += 0.5.1 = + +_Release date: 2026-08-15_ + +* Stabilization release: full code/security audit, compatibility scans, and documentation alignment. Real floors declared: WordPress 4.4+, PHP 8.0+ (previously declared 4.7/7.4). +* Connection test and catalog fetch now check the HTTP status code — an unauthorized or failing store reports an error instead of "Connected" / an empty catalog (non-200 responses are no longer cached). +* API-key field validates the key format before storing. +* Premium plugins are not offered as native updates when no API key is configured (they would only fail with HTTP 403). +* Domain normalization for premium package URLs strips only the literal `www.` prefix (hosts starting with "w" were mangled). +* Opt-in uninstall now also purges the WordPress update transient (premium entries carry the API key in their package URL) and drops a phantom option. + = 0.5.0 = _Release date: 2026-08-14_ diff --git a/robotstxt-manager.php b/robotstxt-manager.php index 69a2643..dbf7d6d 100644 --- a/robotstxt-manager.php +++ b/robotstxt-manager.php @@ -3,9 +3,9 @@ * Plugin Name: Manager (by ROBOTSTXT) * Plugin URI: https://git.robotstxt.es/ROBOTSTXT/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: 0.5.0 - * Requires at least: 4.7 - * Requires PHP: 7.4 + * Version: 0.5.1 + * Requires at least: 4.4 + * Requires PHP: 8.0 * Author: ROBOTSTXT * Author URI: https://www.robotstxt.es/ * License: GPL-3.0-or-later @@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** Plugin version. */ -define( 'ROBOTSTXT_MANAGER_VERSION', '0.5.0' ); +define( 'ROBOTSTXT_MANAGER_VERSION', '0.5.1' ); /** Absolute path to the plugin directory, with trailing slash. */ define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) ); diff --git a/uninstall.php b/uninstall.php index fdf86d4..2f75539 100644 --- a/uninstall.php +++ b/uninstall.php @@ -18,7 +18,6 @@ $option_keys = array( 'robotstxt_manager_api_key', 'robotstxt_manager_cache_ttl_minutes', 'robotstxt_manager_delete_data_on_uninstall', - 'robotstxt_manager_db_version', ); foreach ( $option_keys as $key ) { @@ -26,3 +25,7 @@ foreach ( $option_keys as $key ) { } delete_transient( 'robotstxt_manager_catalog' ); + +// Purge the WordPress update transient: premium entries carry the API key +// in their package URL. WordPress rebuilds it on the next update check. +delete_site_transient( 'update_plugins' ); diff --git a/vendor/autoload.php b/vendor/autoload.php index 30d2faf..38a9b43 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -19,4 +19,4 @@ if (PHP_VERSION_ID < 50600) { require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef::getLoader(); +return ComposerAutoloaderInit891e1a90cdf8723a2ca296aab35c6101::getLoader(); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index d53f30b..036374e 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef +class ComposerAutoloaderInit891e1a90cdf8723a2ca296aab35c6101 { private static $loader; @@ -24,12 +24,12 @@ class ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef require __DIR__ . '/platform_check.php'; - spl_autoload_register(array('ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInit891e1a90cdf8723a2ca296aab35c6101', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); - spl_autoload_unregister(array('ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit891e1a90cdf8723a2ca296aab35c6101', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit21193cc5ee16adfbe1fcd1cc267e45ef::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInit891e1a90cdf8723a2ca296aab35c6101::getInitializer($loader)); $loader->register(true); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 7d5ea49..d8902c3 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInit21193cc5ee16adfbe1fcd1cc267e45ef +class ComposerStaticInit891e1a90cdf8723a2ca296aab35c6101 { public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', @@ -22,7 +22,7 @@ class ComposerStaticInit21193cc5ee16adfbe1fcd1cc267e45ef public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->classMap = ComposerStaticInit21193cc5ee16adfbe1fcd1cc267e45ef::$classMap; + $loader->classMap = ComposerStaticInit891e1a90cdf8723a2ca296aab35c6101::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index d2225c7..a70ba47 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 70400)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 80000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; } if ($issues) {