This commit is contained in:
Javier Casares 2026-06-03 06:31:47 +00:00
commit 7b7fff5246
36 changed files with 610 additions and 31 deletions

View file

@ -153,7 +153,7 @@
var targetId = $( this ).data( 'target' ); var targetId = $( this ).data( 'target' );
var $row = $( '#' + targetId ); var $row = $( '#' + targetId );
$row.prop( 'hidden', function ( i, v ) { return !v; } ); $row.prop( 'hidden', function ( i, v ) { return !v; } );
$( this ).text( $row.prop( 'hidden' ) ? 'Dismiss' : 'Cancel' ); $( this ).text( $row.prop( 'hidden' ) ? mraAlerts.i18n.dismiss : mraAlerts.i18n.cancel );
} ); } );
// Cancel button inside dismiss form row. // Cancel button inside dismiss form row.
@ -161,7 +161,7 @@
var $row = $( this ).closest( 'tr.mra-dismiss-row' ); var $row = $( this ).closest( 'tr.mra-dismiss-row' );
var rowId = $row.attr( 'id' ); var rowId = $row.attr( 'id' );
$row.prop( 'hidden', true ); $row.prop( 'hidden', true );
$( '[data-target="' + rowId + '"]' ).text( 'Dismiss' ); $( '[data-target="' + rowId + '"]' ).text( mraAlerts.i18n.dismiss );
} ); } );
} ); } );
} ( jQuery ) ); } ( jQuery ) );

View file

@ -1,5 +1,17 @@
== Changelog == == Changelog ==
= 1.7.1 =
_Release date: 2026-06-02_
**Fixed**
* Dismissal timestamps are now stored in UTC, consistent with all other plugin timestamps (`dismissed_at`).
* Settings option (`robotstxt_mediaaudit_settings`) no longer autoloads on every page request, reducing the autoloaded options payload.
* Alert count on the Media Audit dashboard is now cached with a transient (1 hour TTL) and invalidated automatically on dismiss, reactivate, scan, purge, and full reset operations.
* "Dismiss" and "Cancel" button labels on the Alerts page are now translatable via `wp_localize_script`.
* Added direct file access guard (`if ( ! defined( 'ABSPATH' ) ) { exit; }`) to all PHP class files in `includes/`.
= 1.7.0 = = 1.7.0 =
_Release date: 2026-05-11_ _Release date: 2026-05-11_

View file

@ -130,11 +130,12 @@ class AlertsPage {
notes = VALUES(notes)", notes = VALUES(notes)",
$attachment_id, $attachment_id,
$user_id, $user_id,
current_time( 'mysql' ), current_time( 'mysql', true ),
$notes $notes
) )
); );
delete_transient( 'mra_alert_count' );
self::invalidate_cache(); self::invalidate_cache();
} }
@ -155,6 +156,7 @@ class AlertsPage {
array( '%d' ) array( '%d' )
); );
delete_transient( 'mra_alert_count' );
self::invalidate_cache(); self::invalidate_cache();
} }
@ -192,6 +194,17 @@ class AlertsPage {
wp_enqueue_style( 'mra-admin', $base . 'css/media-audit-admin.css', array(), $ver ); wp_enqueue_style( 'mra-admin', $base . 'css/media-audit-admin.css', array(), $ver );
wp_enqueue_script( 'mra-admin', $base . 'js/media-audit-admin.js', array( 'jquery' ), $ver, true ); wp_enqueue_script( 'mra-admin', $base . 'js/media-audit-admin.js', array( 'jquery' ), $ver, true );
wp_localize_script(
'mra-admin',
'mraAlerts',
array(
'i18n' => array(
'dismiss' => __( 'Dismiss', 'robotstxt-mediaaudit' ),
'cancel' => __( 'Cancel', 'robotstxt-mediaaudit' ),
),
)
);
} }
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Admin; namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\External\HostnameFilter; use MediaRightsAudit\External\HostnameFilter;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Admin; namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AlertsPage;
use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\Admin\AttachmentDetailPage;
use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\External\ExternalScanner;
@ -928,6 +932,11 @@ class AuditPage {
* @return int * @return int
*/ */
private static function get_alert_attachment_count(): int { private static function get_alert_attachment_count(): int {
$cached = get_transient( 'mra_alert_count' );
if ( is_int( $cached ) ) {
return $cached;
}
global $wpdb; global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@ -937,6 +946,7 @@ class AuditPage {
); );
if ( ! is_array( $rows ) ) { if ( ! is_array( $rows ) ) {
set_transient( 'mra_alert_count', 0, HOUR_IN_SECONDS );
return 0; return 0;
} }
@ -983,6 +993,7 @@ class AuditPage {
} }
} }
set_transient( 'mra_alert_count', $alert_count, HOUR_IN_SECONDS );
return $alert_count; return $alert_count;
} }
} }

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Admin; namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AlertsPage;
use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\Admin\AttachmentDetailPage;
use MediaRightsAudit\External\HostnameFilter; use MediaRightsAudit\External\HostnameFilter;

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Admin; namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Registers and renders the plugin Settings page with tab navigation. * Registers and renders the plugin Settings page with tab navigation.
* *
@ -233,6 +237,7 @@ class Settings {
'type' => 'array', 'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize' ), 'sanitize_callback' => array( $this, 'sanitize' ),
'default' => array(), 'default' => array(),
'autoload' => false,
) )
); );

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Admin; namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Database; use MediaRightsAudit\Core\Database;
use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\Core\Queue\Scheduler;
use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\External\ExternalScanner;

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\CLI; namespace MediaRightsAudit\CLI;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Database; use MediaRightsAudit\Core\Database;
use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\External\ExternalScanner;
use MediaRightsAudit\Internal\AttachmentIndexer; use MediaRightsAudit\Internal\AttachmentIndexer;

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Core; namespace MediaRightsAudit\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\Settings; use MediaRightsAudit\Admin\Settings;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Core; namespace MediaRightsAudit\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Manages all custom table creation and schema migrations. * Manages all custom table creation and schema migrations.
* *
@ -86,6 +90,8 @@ class Database {
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_usage`" ); $wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_usage`" );
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_index`" ); $wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_index`" );
// phpcs:enable WordPress.DB.DirectDatabaseQuery // phpcs:enable WordPress.DB.DirectDatabaseQuery
delete_transient( 'mra_alert_count' );
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Core; namespace MediaRightsAudit\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\Core\Queue\Scheduler;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Core; namespace MediaRightsAudit\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AlertsPage;
use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\Admin\AttachmentDetailPage;
use MediaRightsAudit\Admin\AuditPage; use MediaRightsAudit\Admin\AuditPage;

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Core\Queue; namespace MediaRightsAudit\Core\Queue;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Thin wrapper around Action Scheduler that namespaces all jobs under a single group. * Thin wrapper around Action Scheduler that namespaces all jobs under a single group.
* *

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Provides rate-limited scanning via a transient-based per-minute counter. * Provides rate-limited scanning via a transient-based per-minute counter.
* *

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\Core\Queue\Scheduler;
/** /**
@ -145,6 +149,8 @@ class ExternalScanner {
self::scan_batch( $batch_size ); self::scan_batch( $batch_size );
delete_transient( 'mra_alert_count' );
if ( self::get_pending_count() > 0 ) { if ( self::get_pending_count() > 0 ) {
Scheduler::schedule_single( self::AS_HOOK ); Scheduler::schedule_single( self::AS_HOOK );
} }
@ -361,6 +367,8 @@ class ExternalScanner {
) )
); );
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare // phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
delete_transient( 'mra_alert_count' );
} }
/** /**
@ -381,6 +389,8 @@ class ExternalScanner {
); );
// phpcs:enable WordPress.DB.DirectDatabaseQuery // phpcs:enable WordPress.DB.DirectDatabaseQuery
delete_transient( 'mra_alert_count' );
return $count; return $count;
} }

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\Settings; use MediaRightsAudit\Admin\Settings;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Classifies hostnames against the site's configured alert (include) and * Classifies hostnames against the site's configured alert (include) and
* ignored (exclude) lists stored in the robotstxt_mediaaudit_settings option. * ignored (exclude) lists stored in the robotstxt_mediaaudit_settings option.

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\Settings; use MediaRightsAudit\Admin\Settings;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Aggregates external scan results across multiple providers to surface * Aggregates external scan results across multiple providers to surface
* domains that appear in more than one provider's findings (consensus signals). * domains that appear in more than one provider's findings (consensus signals).

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Immutable result returned by AbstractProvider::scan(). * Immutable result returned by AbstractProvider::scan().
*/ */

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\External; namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\Settings; use MediaRightsAudit\Admin\Settings;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Internal; namespace MediaRightsAudit\Internal;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\Core\Queue\Scheduler;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Internal; namespace MediaRightsAudit\Internal;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\Core\Queue\Scheduler;
/** /**

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Privacy; namespace MediaRightsAudit\Privacy;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Erases Media Audit data for a given user via the WordPress privacy tools. * Erases Media Audit data for a given user via the WordPress privacy tools.
* *

View file

@ -7,6 +7,10 @@
namespace MediaRightsAudit\Privacy; namespace MediaRightsAudit\Privacy;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** /**
* Exports Media Audit data for a given user via the WordPress privacy tools. * Exports Media Audit data for a given user via the WordPress privacy tools.
* *

View file

@ -1,11 +1,11 @@
=== Media Audit (by ROBOTSTXT) === === Media Audit (by ROBOTSTXT) ===
Contributors: javiercasares, robotstxt Contributors: javiercasares, robotstxt
Tags: media, copyright, images, reverse image search, media library Tags: media, copyright, images, reverse image search, media library
Requires at least: 6.8 Requires at least: 5.3
Tested up to: 7.0 Tested up to: 7.0
Requires PHP: 8.2 Requires PHP: 8.0
Requires Plugins: action-scheduler Requires Plugins: action-scheduler
Stable tag: 1.7.0 Stable tag: 1.7.1
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

View file

@ -3,10 +3,10 @@
* Plugin Name: Media Audit (by ROBOTSTXT) * Plugin Name: Media Audit (by ROBOTSTXT)
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit * Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit
* Description: Internal media library usage auditing and external reverse image search to detect potential copyright issues. * Description: Internal media library usage auditing and external reverse image search to detect potential copyright issues.
* Version: 1.7.0 * Version: 1.7.1
* Requires at least: 6.8 * Requires at least: 5.3
* Tested up to: 7.0 * Tested up to: 7.0
* Requires PHP: 8.2 * Requires PHP: 8.0
* Requires Plugins: action-scheduler * Requires Plugins: action-scheduler
* Author: ROBOTSTXT * Author: ROBOTSTXT
* Author URI: https://www.robotstxt.es/ * Author URI: https://www.robotstxt.es/
@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.7.0' ); define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.7.1' );
define( 'ROBOTSTXT_MEDIAAUDIT_DB_VERSION', '1.2.0' ); define( 'ROBOTSTXT_MEDIAAUDIT_DB_VERSION', '1.2.0' );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE', __FILE__ ); define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

383
robotstxt-updater.php Normal file
View file

@ -0,0 +1,383 @@
<?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' );
}
}
}

7
vendor/autoload.php vendored
View file

@ -14,12 +14,9 @@ if (PHP_VERSION_ID < 50600) {
echo $err; echo $err;
} }
} }
trigger_error( throw new RuntimeException($err);
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2::getLoader(); return ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757::getLoader();

View file

@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/ */
private static $installed; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@ -309,6 +320,24 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
} }
/** /**
@ -322,19 +351,27 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php'; $required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required; self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $installed[] = $required;
self::$installed = $installed[count($installed) - 1]; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
} }
} }
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
} }
} }
@ -350,7 +387,7 @@ class InstalledVersions
} }
} }
if (self::$installed !== array()) { if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed; $installed[] = self::$installed;
} }

View file

@ -1,3 +1,4 @@
Copyright (c) Nils Adermann, Jordi Boggiano Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@ -17,3 +18,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.

View file

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer // autoload_real.php @generated by Composer
class ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2 class ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757
{ {
private static $loader; private static $loader;
@ -22,12 +22,14 @@ class ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2
return self::$loader; return self::$loader;
} }
spl_autoload_register(array('ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2', 'loadClassLoader'), true, true); require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2', 'loadClassLoader')); spl_autoload_unregister(array('ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php'; require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::getInitializer($loader)); call_user_func(\Composer\Autoload\ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::getInitializer($loader));
$loader->register(true); $loader->register(true);

View file

@ -4,7 +4,7 @@
namespace Composer\Autoload; namespace Composer\Autoload;
class ComposerStaticInit957728ab3efa005f456e5f9df13a19d2 class ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757
{ {
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'M' => 'M' =>
@ -51,9 +51,9 @@ class ComposerStaticInit957728ab3efa005f456e5f9df13a19d2
public static function getInitializer(ClassLoader $loader) public static function getInitializer(ClassLoader $loader)
{ {
return \Closure::bind(function () use ($loader) { return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$prefixLengthsPsr4; $loader->prefixLengthsPsr4 = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$prefixDirsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$classMap; $loader->classMap = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$classMap;
}, null, ClassLoader::class); }, null, ClassLoader::class);
} }

View file

@ -1,6 +1,6 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'robotstxt/mediaaudit', 'name' => 'robotstxt/robotstxt-mediaaudit',
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0', 'version' => '1.0.0.0',
'reference' => null, 'reference' => null,
@ -10,7 +10,7 @@
'dev' => false, 'dev' => false,
), ),
'versions' => array( 'versions' => array(
'robotstxt/mediaaudit' => array( 'robotstxt/robotstxt-mediaaudit' => array(
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0', 'version' => '1.0.0.0',
'reference' => null, 'reference' => null,

25
vendor/composer/platform_check.php vendored Normal file
View file

@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}