This commit is contained in:
Javier Casares 2026-08-17 18:51:45 +00:00
commit 7f89e98bbc
18 changed files with 2227 additions and 1853 deletions

View file

@ -1,5 +1,49 @@
== Changelog ==
= 1.8.3 =
_Release date: 2026-08-17_
**Highlights**
* Plugin updates are now delivered through [Manager (by ROBOTSTXT)](https://www.robotstxt.software/plugins/robotstxt-manager/) instead of a bundled self-updater.
**Added**
* Dismissible admin notice on the site and Network Admin Plugins pages when Manager (by ROBOTSTXT) is not installed and active, linking to `https://www.robotstxt.software/plugins/robotstxt-manager/` (dismissible per user; re-appears if the Manager is later removed).
* Permanent (non-dismissible) notice on the site and network Settings pages while Manager is not available.
* `Update URI` plugin header pointing to `https://www.robotstxt.software/plugins/robotstxt-mediaaudit/`.
* Header tests now also cover `Plugin URI`, `Update URI`, `Author`, `Author URI`, `Contributors`, contributor ordering, and absence of self-updater artifacts.
**Fixed**
* The dismissible Manager notice now also renders on the Network Admin plugins page (screen base `plugins-network`), not only on site-level Plugins.
* `uninstall.php` now always removes the per-user notice-dismissal user meta (UI state, not user data).
**Changed**
* Plugin URI and Author URI now point to `www.robotstxt.software`; `robotstxt` is listed first in the Contributors headers.
* `bin/preflight.sh` allowlist updated: `update.json` and `robotstxt-updater.php` are forbidden in the release ZIP.
* Dev tooling updated via `composer update` (PHPCS 3.13.6, WPCS 3.4.1, PHPStan 2.2.8, PHPUnit 9.6.36); PHPStan fix for transient cache typing in the Network Tools status reader.
* Translations completed for Spanish (es_ES) and Catalan (ca): all fuzzy and untranslated strings through 1.8.2 are now translated (326/326 in both locales).
**Removed**
* Bundled self-updater files `robotstxt-updater.php` and `update.json`.
**Compatibility**
* WordPress: 5.3 - 7.1
* PHP: 8.0 - 8.5
* WP-CLI: 2.x
**Tests**
* PHPCS (WordPress-Core, WordPress-Docs, WordPress-Extra): PASS
* PHPStan level 9: PASS
* PHPCompatibility 8.0-8.5: PASS
* PHPUnit: 11 tests, 54 assertions
= 1.8.2 =
_Release date: 2026-08-08_

View file

@ -0,0 +1,209 @@
<?php
/**
* Manager plugin dependency notice.
*
* Media Audit ships without a self-updater; updates are delivered by the
* Manager (by ROBOTSTXT) plugin. This component nudges the administrator
* when Manager is missing: a dismissible notice on the Plugins page and a
* permanent inline notice on the plugin Settings pages.
*
* @package MediaRightsAudit\Admin
*/
namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Shows update-manager dependency notices.
*/
final class ManagerNotice {
/**
* Manager plugin folder slug.
*/
public const MANAGER_SLUG = 'robotstxt-manager';
/**
* Manager plugin page URL.
*/
public const MANAGER_URL = 'https://www.robotstxt.software/plugins/robotstxt-manager/';
/**
* User-meta key that records a dismissed notice (per user).
*/
private const DISMISS_KEY = 'robotstxt_mediaaudit_manager_notice_dismissed';
/**
* GET argument used by the dismiss link.
*/
private const DISMISS_ARG = 'robotstxt_mediaaudit_dismiss_manager_notice';
/**
* Nonce action for the dismiss link.
*/
private const DISMISS_ACTION = 'robotstxt_mediaaudit_dismiss_manager_notice';
/**
* Registers the admin hooks (site and network admin).
*
* @return void
*/
public static function register_hooks(): void {
add_action( 'admin_notices', array( __CLASS__, 'render_plugins_page_notice' ) );
add_action( 'network_admin_notices', array( __CLASS__, 'render_plugins_page_notice' ) );
add_action( 'admin_init', array( __CLASS__, 'handle_dismiss' ) );
}
/**
* Indicates whether the Manager plugin is installed and active.
*
* Checks site-level and network-wide activation, plus a filter so the
* Manager itself can report its own state.
*
* @return bool True when Manager is active.
*/
public static function is_manager_active(): bool {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'is_plugin_active' ) ) {
return false;
}
$active = false;
// Canonical main file first (fast path).
$basename = self::MANAGER_SLUG . '/' . self::MANAGER_SLUG . '.php';
if ( is_plugin_active( $basename ) || is_plugin_active_for_network( $basename ) ) {
$active = true;
}
// Cover any other main-file name inside the canonical folder.
if ( ! $active && function_exists( 'get_plugins' ) ) {
foreach ( array_keys( get_plugins( '/' . self::MANAGER_SLUG ) ) as $file ) {
if ( is_plugin_active( self::MANAGER_SLUG . '/' . $file ) ) {
$active = true;
break;
}
}
}
/**
* Filters whether the Manager (by ROBOTSTXT) plugin is active.
*
* @param bool $active Whether Manager is detected as active.
*/
return (bool) apply_filters( 'robotstxt_mediaaudit_manager_active', $active );
}
/**
* Renders the dismissible notice on the Plugins page when Manager is missing.
*
* @return void
*/
public static function render_plugins_page_notice(): void {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen instanceof \WP_Screen || ! in_array( $screen->base, array( 'plugins', 'plugins-network' ), true ) ) {
return;
}
if ( self::is_manager_active() ) {
return;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
if ( '1' === get_user_meta( get_current_user_id(), self::DISMISS_KEY, true ) ) {
return;
}
$dismiss_url = wp_nonce_url(
add_query_arg( self::DISMISS_ARG, 1 ),
self::DISMISS_ACTION
);
echo '<div class="notice notice-warning"><p>'
. wp_kses( self::message(), array( 'a' => array( 'href' => true ) ) )
. ' <a href="' . esc_url( $dismiss_url ) . '" class="button button-small">' . esc_html__( 'Dismiss', 'robotstxt-mediaaudit' ) . '</a>'
. '</p></div>';
}
/**
* Renders the permanent inline notice for the Settings pages (not dismissible).
*
* Used on both the site Settings page and the Network Settings page.
*
* @return void
*/
public static function render_settings_notice(): void {
if ( self::is_manager_active() ) {
return;
}
echo '<div class="notice notice-warning inline" style="margin:16px 0;">'
. '<p>' . wp_kses( self::message(), array( 'a' => array( 'href' => true ) ) ) . '</p>'
. '</div>';
}
/**
* Handles the dismiss link: verifies the nonce, stores the dismissal (PRG).
*
* Also clears a stored dismissal once Manager is active again, so the
* notice returns if Manager is ever removed.
*
* @return void
*/
public static function handle_dismiss(): void {
$user_id = get_current_user_id();
$dismissed = $user_id && get_user_meta( $user_id, self::DISMISS_KEY, true );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- presence check only; state changes require the nonce below.
if ( ! isset( $_GET[ self::DISMISS_ARG ] ) && ! $dismissed ) {
return;
}
if ( self::is_manager_active() ) {
if ( $dismissed ) {
delete_user_meta( $user_id, self::DISMISS_KEY );
}
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no state change without the nonce check below.
if ( ! isset( $_GET[ self::DISMISS_ARG ] ) ) {
return;
}
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
check_admin_referer( self::DISMISS_ACTION );
update_user_meta( $user_id, self::DISMISS_KEY, '1' );
wp_safe_redirect( remove_query_arg( array( self::DISMISS_ARG, '_wpnonce' ) ) );
exit;
}
/**
* Builds the shared notice message (translation with a link placeholder).
*
* The result must be passed through wp_kses() at output time.
*
* @return string Unescaped message HTML.
*/
private static function message(): string {
return sprintf(
/* translators: %s: Manager plugin page URL. */
__( 'To receive plugin updates, the plugin <a href="%s">Manager (by ROBOTSTXT)</a> must be installed and active.', 'robotstxt-mediaaudit' ),
esc_url( self::MANAGER_URL )
);
}
}

View file

@ -963,6 +963,8 @@ class Settings {
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php ManagerNotice::render_settings_notice(); ?>
<nav class="nav-tab-wrapper">
<?php foreach ( $tabs as $key => $label ) : ?>
<a

View file

@ -14,6 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
use MediaRightsAudit\Admin\AlertsPage;
use MediaRightsAudit\Admin\AttachmentDetailPage;
use MediaRightsAudit\Admin\AuditPage;
use MediaRightsAudit\Admin\ManagerNotice;
use MediaRightsAudit\Admin\Settings;
use MediaRightsAudit\Admin\ToolsPage;
use MediaRightsAudit\CLI\Command;
@ -95,6 +96,7 @@ class Plugin {
add_filter( 'mra/external/providers', array( $this, 'register_providers' ) );
add_filter( 'wp_privacy_personal_data_exporters', array( DataExporter::class, 'register' ) );
add_filter( 'wp_privacy_personal_data_erasers', array( DataEraser::class, 'register' ) );
ManagerNotice::register_hooks();
$this->audit_page->register_hooks();
$this->register_cli_commands();
}

View file

@ -210,6 +210,8 @@ class Settings {
<div class="wrap">
<h1><?php esc_html_e( 'Media Audit — Network Settings', 'robotstxt-mediaaudit' ); ?></h1>
<?php \MediaRightsAudit\Admin\ManagerNotice::render_settings_notice(); ?>
<?php if ( $updated ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Settings saved.', 'robotstxt-mediaaudit' ); ?></p></div>
<?php endif; ?>

View file

@ -398,9 +398,16 @@ class ToolsPage {
if ( is_array( $cached ) ) {
$typed = array();
foreach ( $cached as $k => $v ) {
if ( is_int( $k ) && is_array( $v ) ) {
$typed[ $k ] = $v;
if ( ! is_int( $k ) || ! is_array( $v ) ) {
continue;
}
$row = array();
foreach ( $v as $row_key => $row_value ) {
if ( is_string( $row_key ) ) {
$row[ $row_key ] = $row_value;
}
}
$typed[ $k ] = $row;
}
return $typed;
}

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,12 @@
=== Media Audit (by ROBOTSTXT) ===
Contributors: javiercasares, robotstxt
Contributors: robotstxt, javiercasares
Tags: media, copyright, images, reverse image search, media library
Requires at least: 5.3
Tested up to: 7.1
Requires PHP: 8.0
Requires Plugins: action-scheduler
Stable tag: 1.8.2
Stable tag: 1.8.3
Version: 1.8.3
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -29,6 +30,7 @@ Media Audit helps site owners and editors understand where their media files are
= Requirements =
* [Action Scheduler](https://actionscheduler.org/) plugin (declared as a required dependency).
* [Manager (by ROBOTSTXT)](https://www.robotstxt.software/plugins/robotstxt-manager/) plugin (recommended) — required to receive plugin updates.
* Google Cloud Vision API key (for Web Detection scanning).
* TinEye Commercial API key (for reverse image search).
@ -97,7 +99,13 @@ By default, no. Enable **Settings → Delete data on uninstall** if you want all
== Changelog ==
Only the 3 latest versions. The full changelog is in [changelog.txt](changelog.txt).
Only the 3 latest versions. The full changelog is at [www.robotstxt.software/plugins/robotstxt-mediaaudit/](https://www.robotstxt.software/plugins/robotstxt-mediaaudit/).
= 1.8.3 =
* Updates are now delivered through [Manager (by ROBOTSTXT)](https://www.robotstxt.software/plugins/robotstxt-manager/): the bundled self-updater (`robotstxt-updater.php`, `update.json`) was removed.
* The Plugins page now shows a dismissible notice when Manager is not installed and active, and the Settings page shows a permanent notice until it is.
* New `Update URI` header pointing to the plugin page at www.robotstxt.software; Plugin and Author URIs updated accordingly.
= 1.8.2 =
@ -114,16 +122,9 @@ Only the 3 latest versions. The full changelog is in [changelog.txt](changelog.t
* Removed unreliable in-render script enqueueing for Network Tools page; operation buttons now work without JavaScript.
* JS dismiss/cancel labels on the Alerts page now use i18n strings from `wp_localize_script`.
= 1.8.0 =
* **WordPress Multisite support** — plugin now declares `Network: true` and can be activated network-wide, with two operating modes (per site / central) configurable from Network Admin → Media Audit → Settings.
* Network admin panel (central mode): aggregated Audit list, Alerts list, and Tools page showing all media across all sites, plus a network mirror table for fast cross-site queries.
* New site provisioning via `wp_initialize_site`; settings migration on first switch to central mode; network-aware uninstall.
* DB schema version bumped to `1.3.0`.
= Previous versions =
See [changelog.txt](changelog.txt) for the full history.
If you want to see the full changelog, visit [www.robotstxt.software/plugins/robotstxt-mediaaudit/](https://www.robotstxt.software/plugins/robotstxt-mediaaudit/).
== Compliance ==

View file

@ -1,17 +1,18 @@
<?php
/**
* Plugin Name: Media Audit (by ROBOTSTXT)
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit
* Plugin URI: https://www.robotstxt.software/plugins/robotstxt-mediaaudit/
* Description: Internal media library usage auditing and external reverse image search to detect potential copyright issues.
* Version: 1.8.2
* Version: 1.8.3
* Requires at least: 5.3
* Tested up to: 7.1
* Requires PHP: 8.0
* Requires Plugins: action-scheduler
* Update URI: https://www.robotstxt.software/plugins/robotstxt-mediaaudit/
* Network: true
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.es/
* Contributors: javiercasares, robotstxt
* Author URI: https://www.robotstxt.software/
* Contributors: robotstxt, javiercasares
* License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.txt
* Text Domain: robotstxt-mediaaudit
@ -24,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.8.2' );
define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.8.3' );
define( 'ROBOTSTXT_MEDIAAUDIT_DB_VERSION', '1.3.0' );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

View file

@ -1,383 +0,0 @@
<?php
/**
* Generic JSON-based updater for ROBOTSTXT plugins.
*
* This file is designed to be copied to any ROBOTSTXT plugin.
* It auto-configures itself by reading the plugin headers.
*
* @package ROBOTSTXT
* @version 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Robotstxt_Updater' ) ) {
/**
* Class Robotstxt_Updater
*
* Generic updater that works with any plugin.
* Reads plugin headers and constructs update URL automatically.
*/
class Robotstxt_Updater {
/**
* Plugin file path.
*
* @var string
*/
private string $plugin_file_path;
/**
* Plugin basename (e.g., 'my-plugin/my-plugin.php').
*
* @var string
*/
private string $plugin_basename;
/**
* Plugin slug (directory name).
*
* @var string
*/
private string $plugin_slug;
/**
* Remote JSON URL.
*
* @var string
*/
private string $json_url;
/**
* Cache key.
*
* @var string
*/
private string $cache_key;
/**
* Plugin headers.
*
* @var array
*/
private array $plugin_data;
/**
* Initialize the updater.
*
* Usage in your main plugin file:
* require_once __DIR__ . '/robotstxt-updater.php';
* Robotstxt_Updater::init( __FILE__ );
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*/
public static function init( string $plugin_file_path ): void {
$instance = new self( $plugin_file_path );
$instance->register();
}
/**
* Constructor.
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*/
private function __construct( string $plugin_file_path ) {
$this->plugin_file_path = $plugin_file_path;
$this->plugin_basename = plugin_basename( $plugin_file_path );
$this->plugin_slug = dirname( $this->plugin_basename );
$this->plugin_data = $this->get_plugin_data();
$this->json_url = $this->build_json_url();
$this->cache_key = 'robotstxt_updater_' . md5( $this->plugin_basename );
}
/**
* Register WordPress hooks.
*/
private function register(): void {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update_info' ) );
add_filter( 'plugins_api', array( $this, 'provide_plugin_details' ), 10, 3 );
add_action( 'admin_init', array( $this, 'handle_cache_clear' ) );
add_action( 'robotstxt_updater_clear_cache', array( $this, 'clear_cache' ) );
}
/**
* Get plugin headers.
*
* @return array Plugin data.
*/
private function get_plugin_data(): array {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugin_data( $this->plugin_file_path, false, false );
}
/**
* Build JSON URL from plugin headers.
*
* Tries to use "Gitea Plugin URI" header to construct the URL.
* Falls back to Plugin URI if Gitea URI is not available.
*
* @return string JSON URL.
*/
private function build_json_url(): string {
// Try Gitea Plugin URI (format: "OWNER/REPO" or full URL).
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) ) {
$gitea_uri = $this->plugin_data['Gitea Plugin URI'];
// If it's already a full URL, use it.
if ( str_starts_with( $gitea_uri, 'http' ) ) {
// Extract base URL and construct JSON path.
return rtrim( $gitea_uri, '/' ) . '/raw/branch/main/update.json';
}
// If it's in format "OWNER/REPO", construct full URL.
if ( preg_match( '#^[^/]+/[^/]+$#', $gitea_uri ) ) {
return "https://git.robotstxt.es/{$gitea_uri}/raw/branch/main/update.json";
}
}
// Fallback: try to extract from Plugin URI.
if ( ! empty( $this->plugin_data['PluginURI'] ) ) {
$plugin_uri = $this->plugin_data['PluginURI'];
if ( str_contains( $plugin_uri, 'git.robotstxt.es' ) ) {
return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json';
}
}
// Last resort: construct from plugin slug.
return "https://git.robotstxt.es/ROBOTSTXT/{$this->plugin_slug}/raw/branch/main/update.json";
}
/**
* Inject update info into WP's plugin update transient.
*
* @param object|mixed $transient The update_plugins transient.
*
* @return object The modified transient.
*/
public function inject_update_info( $transient ) {
if ( ! is_object( $transient ) ) {
$transient = new stdClass();
}
if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
return $transient;
}
if ( empty( $transient->checked[ $this->plugin_basename ] ) ) {
return $transient;
}
$current_version = $transient->checked[ $this->plugin_basename ];
$remote = $this->get_remote_data();
if ( empty( $remote['version'] ) || empty( $remote['download_url'] ) ) {
return $transient;
}
if ( ! $this->is_compatible( $remote ) ) {
return $transient;
}
if ( version_compare( $remote['version'], $current_version, '>' ) ) {
$update = (object) array(
'slug' => $remote['slug'] ?? $this->plugin_slug,
'plugin' => $this->plugin_basename,
'new_version' => $remote['version'],
'url' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
'package' => $remote['download_url'],
'tested' => $remote['tested'] ?? '',
'requires' => $remote['requires'] ?? '',
'requires_php' => $remote['requires_php'] ?? '',
);
$transient->response[ $this->plugin_basename ] = $update;
}
return $transient;
}
/**
* Provide "View details" modal content.
*
* @param false|object|array $result The result object or array.
* @param string $action The type of information being requested.
* @param object $args Plugin API arguments.
*
* @return false|object The plugin information object or false.
*/
public function provide_plugin_details( $result, string $action, object $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
if ( empty( $args->slug ) || $args->slug !== $this->plugin_slug ) {
return $result;
}
$remote = $this->get_remote_data();
if ( empty( $remote['version'] ) ) {
return $result;
}
return (object) array(
'name' => $remote['name'] ?? $this->plugin_data['Name'] ?? $this->plugin_slug,
'slug' => $remote['slug'] ?? $this->plugin_slug,
'version' => $remote['version'],
'author' => $remote['author'] ?? $this->plugin_data['Author'] ?? '',
'homepage' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
'requires' => $remote['requires'] ?? '',
'tested' => $remote['tested'] ?? '',
'requires_php' => $remote['requires_php'] ?? '',
'sections' => array(
'description' => $remote['description'] ?? $this->plugin_data['Description'] ?? '',
'changelog' => $remote['changelog'] ?? '',
),
'download_link' => $remote['download_url'] ?? '',
);
}
/**
* Get remote data with caching and HMAC signature verification.
*
* @return array Remote data.
*/
private function get_remote_data(): array {
$cached = get_site_transient( $this->cache_key );
// Verify HMAC signature if AUTH_SALT is defined and cache has signature.
if ( false !== $cached && defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
if ( hash_equals( $expected_sig, $cached['signature'] ) ) {
// Signature valid, return data.
return is_array( $cached['data'] ) ? $cached['data'] : array();
}
// Signature invalid, delete corrupted cache.
delete_site_transient( $this->cache_key );
$cached = false;
}
}
// If no valid cache, fetch fresh data.
if ( false === $cached ) {
$remote = $this->fetch_json();
// Store with HMAC signature if AUTH_SALT is available.
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
$payload = array(
'data' => $remote ?: array(),
'timestamp' => time(),
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ?: array() ), AUTH_SALT ),
);
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
} else {
// Fallback to standard caching.
set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS );
}
return is_array( $remote ) ? $remote : array();
}
// Legacy cache format without signature (backward compatibility).
return is_array( $cached ) ? $cached : array();
}
/**
* Fetch JSON from remote URL.
*
* @return array Decoded JSON data.
*/
private function fetch_json(): array {
$response = wp_remote_get(
$this->json_url,
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
return array();
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return array();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return is_array( $data ) ? $data : array();
}
/**
* Check compatibility.
*
* @param array $remote Remote data.
*
* @return bool True if compatible.
*/
private function is_compatible( array $remote ): bool {
if ( ! empty( $remote['requires_php'] ) ) {
if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) {
return false;
}
}
if ( ! empty( $remote['requires'] ) ) {
if ( version_compare( get_bloginfo( 'version' ), $remote['requires'], '<' ) ) {
return false;
}
}
return true;
}
/**
* Handle manual cache clear via URL parameter.
*/
public function handle_cache_clear(): void {
// Check if this is a cache clear request first.
$clear_cache = filter_input( INPUT_GET, 'robotstxt_clear_update_cache', FILTER_UNSAFE_RAW );
if ( null === $clear_cache ) {
return;
}
// This is a cache clear request - now verify nonce.
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'robotstxt_clear_update_cache' ) ) {
wp_die( esc_html__( 'Security check failed', 'robotstxt-smtp' ) );
}
// Check permissions.
if ( ! current_user_can( 'update_plugins' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-smtp' ) );
}
$this->clear_cache();
wp_safe_redirect( remove_query_arg( array( 'robotstxt_clear_update_cache', '_wpnonce' ) ) );
exit;
}
/**
* Clear update cache.
*/
public function clear_cache(): void {
delete_site_transient( $this->cache_key );
delete_site_transient( 'update_plugins' );
}
}
}

View file

@ -14,6 +14,9 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
global $wpdb;
// User preferences are always removed: they are not user data, they are UI state.
$wpdb->delete( $wpdb->usermeta, array( 'meta_key' => 'robotstxt_mediaaudit_manager_notice_dismissed' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.SlowDBQuery.slow_db_query_meta_key
if ( is_multisite() ) {
// In Multisite, read the delete preference from the network settings option.
$net_raw = get_site_option( 'robotstxt_mediaaudit_network_settings', array() );

View file

@ -1,8 +1,8 @@
{
"name": "Media Audit (by ROBOTSTXT)",
"slug": "robotstxt-mediaaudit",
"version": "1.8.2",
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit/releases/download/1.8.2/robotstxt-mediaaudit-1.8.2.zip",
"version": "1.8.8",
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit/releases/download/1.8.3/robotstxt-mediaaudit-1.8.3.zip",
"requires": "5.3",
"requires_php": "8.0",
"tested": "7.1",
@ -11,10 +11,10 @@
"author_profile": "https://www.robotstxt.es/",
"homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit",
"description": "Audit your media library for copyright risks: track internal usage, run reverse image search via Google Vision, TinEye, and PicDefense, and classify results against configurable alert and ignored hostname lists.",
"changelog": "<h3>1.8.2 - 2026-08-08</h3><ul><li><strong>Changed:</strong> Contribution guidelines restructured into a slim index plus topic-specific files (single source of truth).</li><li><strong>Added:</strong> Automated pre-deploy verification (bin/preflight.sh) and plugin header tests.</li><li><strong>Added:</strong> Clean-context pre-deploy audit tooling (.claude agents and slash command).</li><li><strong>Fixed:</strong> deploy.sh now excludes all AGENTS-*.md dev docs from the release ZIP.</li><li><strong>Compatibility:</strong> WordPress 5.3 - 7.1, PHP 8.0 - 8.5.</li></ul><h3>1.8.1 - 2026-06-08</h3><ul><li><strong>Fixed:</strong> Network Operations buttons now use PRG POST forms instead of AJAX.</li><li><strong>Fixed:</strong> Removed unreliable Network Tools script enqueueing.</li><li><strong>Fixed:</strong> JS dismiss/cancel labels now i18n via wp_localize_script.</li></ul><h3>1.8.0 - 2026-06-08</h3><ul><li><strong>Added:</strong> WordPress Multisite support (Network: true) with per-site and central operating modes.</li><li><strong>Added:</strong> Network admin panel with aggregated audit/alerts/tools and a network mirror table.</li><li><strong>Added:</strong> New site provisioning and network-aware uninstall.</li></ul>",
"changelog": "",
"sections": {
"description": "Audit your media library for copyright risks: track internal usage, run reverse image search via Google Vision, TinEye, and PicDefense, and classify results against configurable alert and ignored hostname lists.",
"changelog": "<h3>1.8.2 - 2026-08-08</h3><ul><li><strong>Changed:</strong> Contribution guidelines restructured into a slim index plus topic-specific files (single source of truth).</li><li><strong>Added:</strong> Automated pre-deploy verification (bin/preflight.sh) and plugin header tests.</li><li><strong>Added:</strong> Clean-context pre-deploy audit tooling (.claude agents and slash command).</li><li><strong>Fixed:</strong> deploy.sh now excludes all AGENTS-*.md dev docs from the release ZIP.</li><li><strong>Compatibility:</strong> WordPress 5.3 - 7.1, PHP 8.0 - 8.5.</li></ul><h3>1.8.1 - 2026-06-08</h3><ul><li><strong>Fixed:</strong> Network Operations buttons now use PRG POST forms instead of AJAX.</li><li><strong>Fixed:</strong> Removed unreliable Network Tools script enqueueing.</li><li><strong>Fixed:</strong> JS dismiss/cancel labels now i18n via wp_localize_script.</li></ul><h3>1.8.0 - 2026-06-08</h3><ul><li><strong>Added:</strong> WordPress Multisite support (Network: true) with per-site and central operating modes.</li><li><strong>Added:</strong> Network admin panel with aggregated audit/alerts/tools and a network mirror table.</li><li><strong>Added:</strong> New site provisioning and network-aware uninstall.</li></ul>"
"changelog": ""
},
"banners": {
"low": "",

View file

@ -10,6 +10,7 @@ return array(
'MediaRightsAudit\\Admin\\AlertsPage' => $baseDir . '/includes/Admin/AlertsPage.php',
'MediaRightsAudit\\Admin\\AttachmentDetailPage' => $baseDir . '/includes/Admin/AttachmentDetailPage.php',
'MediaRightsAudit\\Admin\\AuditPage' => $baseDir . '/includes/Admin/AuditPage.php',
'MediaRightsAudit\\Admin\\ManagerNotice' => $baseDir . '/includes/Admin/ManagerNotice.php',
'MediaRightsAudit\\Admin\\MediaListTable' => $baseDir . '/includes/Admin/MediaListTable.php',
'MediaRightsAudit\\Admin\\Settings' => $baseDir . '/includes/Admin/Settings.php',
'MediaRightsAudit\\Admin\\ToolsPage' => $baseDir . '/includes/Admin/ToolsPage.php',

View file

@ -25,6 +25,7 @@ class ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757
'MediaRightsAudit\\Admin\\AlertsPage' => __DIR__ . '/../..' . '/includes/Admin/AlertsPage.php',
'MediaRightsAudit\\Admin\\AttachmentDetailPage' => __DIR__ . '/../..' . '/includes/Admin/AttachmentDetailPage.php',
'MediaRightsAudit\\Admin\\AuditPage' => __DIR__ . '/../..' . '/includes/Admin/AuditPage.php',
'MediaRightsAudit\\Admin\\ManagerNotice' => __DIR__ . '/../..' . '/includes/Admin/ManagerNotice.php',
'MediaRightsAudit\\Admin\\MediaListTable' => __DIR__ . '/../..' . '/includes/Admin/MediaListTable.php',
'MediaRightsAudit\\Admin\\Settings' => __DIR__ . '/../..' . '/includes/Admin/Settings.php',
'MediaRightsAudit\\Admin\\ToolsPage' => __DIR__ . '/../..' . '/includes/Admin/ToolsPage.php',