This commit is contained in:
Javier Casares 2026-08-15 12:40:32 +00:00
commit 061329c704
11 changed files with 114 additions and 25 deletions

View file

@ -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 );

View file

@ -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.68.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, 8127 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_

View file

@ -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 );

View file

@ -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 );
}
/**

View file

@ -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_

View file

@ -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__ ) );

View file

@ -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' );

2
vendor/autoload.php vendored
View file

@ -19,4 +19,4 @@ if (PHP_VERSION_ID < 50600) {
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit21193cc5ee16adfbe1fcd1cc267e45ef::getLoader();
return ComposerAutoloaderInit891e1a90cdf8723a2ca296aab35c6101::getLoader();

View file

@ -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);

View file

@ -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);
}

View file

@ -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) {