From 7b7fff524658fc1fc23a165c6dc0ff8197123384 Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Wed, 3 Jun 2026 06:31:47 +0000 Subject: [PATCH] v1.7.1 --- assets/js/media-audit-admin.js | 4 +- changelog.txt | 12 + includes/Admin/AlertsPage.php | 15 +- includes/Admin/AttachmentDetailPage.php | 4 + includes/Admin/AuditPage.php | 11 + includes/Admin/MediaListTable.php | 4 + includes/Admin/Settings.php | 5 + includes/Admin/ToolsPage.php | 4 + includes/CLI/Command.php | 4 + includes/Core/Activator.php | 4 + includes/Core/Database.php | 6 + includes/Core/Deactivator.php | 4 + includes/Core/Plugin.php | 4 + includes/Core/Queue/Scheduler.php | 4 + includes/External/AbstractProvider.php | 4 + includes/External/ExternalScanner.php | 10 + includes/External/GoogleVisionProvider.php | 4 + includes/External/HostnameFilter.php | 4 + includes/External/PicDefenseProvider.php | 4 + includes/External/ResultsConsolidator.php | 4 + includes/External/ScanResult.php | 4 + includes/External/TinEyeProvider.php | 4 + includes/Internal/AttachmentIndexer.php | 4 + includes/Internal/UsageScanner.php | 4 + includes/Privacy/DataEraser.php | 4 + includes/Privacy/DataExporter.php | 4 + readme.txt | 6 +- robotstxt-mediaaudit.php | 8 +- robotstxt-updater.php | 383 +++++++++++++++++++++ vendor/autoload.php | 7 +- vendor/composer/InstalledVersions.php | 45 ++- vendor/composer/LICENSE | 2 + vendor/composer/autoload_real.php | 10 +- vendor/composer/autoload_static.php | 12 +- vendor/composer/installed.php | 4 +- vendor/composer/platform_check.php | 25 ++ 36 files changed, 610 insertions(+), 31 deletions(-) create mode 100644 robotstxt-updater.php create mode 100644 vendor/composer/platform_check.php diff --git a/assets/js/media-audit-admin.js b/assets/js/media-audit-admin.js index 69efbb9..696029f 100644 --- a/assets/js/media-audit-admin.js +++ b/assets/js/media-audit-admin.js @@ -153,7 +153,7 @@ var targetId = $( this ).data( 'target' ); var $row = $( '#' + targetId ); $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. @@ -161,7 +161,7 @@ var $row = $( this ).closest( 'tr.mra-dismiss-row' ); var rowId = $row.attr( 'id' ); $row.prop( 'hidden', true ); - $( '[data-target="' + rowId + '"]' ).text( 'Dismiss' ); + $( '[data-target="' + rowId + '"]' ).text( mraAlerts.i18n.dismiss ); } ); } ); } ( jQuery ) ); diff --git a/changelog.txt b/changelog.txt index f1135bd..d120f9b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,17 @@ == 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 = _Release date: 2026-05-11_ diff --git a/includes/Admin/AlertsPage.php b/includes/Admin/AlertsPage.php index 487c5db..31751f7 100644 --- a/includes/Admin/AlertsPage.php +++ b/includes/Admin/AlertsPage.php @@ -130,11 +130,12 @@ class AlertsPage { notes = VALUES(notes)", $attachment_id, $user_id, - current_time( 'mysql' ), + current_time( 'mysql', true ), $notes ) ); + delete_transient( 'mra_alert_count' ); self::invalidate_cache(); } @@ -155,6 +156,7 @@ class AlertsPage { array( '%d' ) ); + delete_transient( 'mra_alert_count' ); self::invalidate_cache(); } @@ -192,6 +194,17 @@ class AlertsPage { 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_localize_script( + 'mra-admin', + 'mraAlerts', + array( + 'i18n' => array( + 'dismiss' => __( 'Dismiss', 'robotstxt-mediaaudit' ), + 'cancel' => __( 'Cancel', 'robotstxt-mediaaudit' ), + ), + ) + ); } /** diff --git a/includes/Admin/AttachmentDetailPage.php b/includes/Admin/AttachmentDetailPage.php index 5c0e725..f3536cb 100644 --- a/includes/Admin/AttachmentDetailPage.php +++ b/includes/Admin/AttachmentDetailPage.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Admin; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\External\HostnameFilter; /** diff --git a/includes/Admin/AuditPage.php b/includes/Admin/AuditPage.php index ba21fee..56435fb 100644 --- a/includes/Admin/AuditPage.php +++ b/includes/Admin/AuditPage.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Admin; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\External\ExternalScanner; @@ -928,6 +932,11 @@ class AuditPage { * @return int */ private static function get_alert_attachment_count(): int { + $cached = get_transient( 'mra_alert_count' ); + if ( is_int( $cached ) ) { + return $cached; + } + global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery @@ -937,6 +946,7 @@ class AuditPage { ); if ( ! is_array( $rows ) ) { + set_transient( 'mra_alert_count', 0, HOUR_IN_SECONDS ); return 0; } @@ -983,6 +993,7 @@ class AuditPage { } } + set_transient( 'mra_alert_count', $alert_count, HOUR_IN_SECONDS ); return $alert_count; } } diff --git a/includes/Admin/MediaListTable.php b/includes/Admin/MediaListTable.php index 3f231df..807e080 100644 --- a/includes/Admin/MediaListTable.php +++ b/includes/Admin/MediaListTable.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Admin; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\External\HostnameFilter; diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 24bd6f8..c3ff740 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Admin; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Registers and renders the plugin Settings page with tab navigation. * @@ -233,6 +237,7 @@ class Settings { 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => array(), + 'autoload' => false, ) ); diff --git a/includes/Admin/ToolsPage.php b/includes/Admin/ToolsPage.php index 63ba567..32634f2 100644 --- a/includes/Admin/ToolsPage.php +++ b/includes/Admin/ToolsPage.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Admin; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Database; use MediaRightsAudit\Core\Queue\Scheduler; use MediaRightsAudit\External\ExternalScanner; diff --git a/includes/CLI/Command.php b/includes/CLI/Command.php index c822b09..fcfabd2 100644 --- a/includes/CLI/Command.php +++ b/includes/CLI/Command.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\CLI; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Database; use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\Internal\AttachmentIndexer; diff --git a/includes/Core/Activator.php b/includes/Core/Activator.php index ca2d357..4ba32ac 100644 --- a/includes/Core/Activator.php +++ b/includes/Core/Activator.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Core; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\Settings; /** diff --git a/includes/Core/Database.php b/includes/Core/Database.php index 259a58a..dd6a12a 100644 --- a/includes/Core/Database.php +++ b/includes/Core/Database.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Core; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * 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_index`" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery + + delete_transient( 'mra_alert_count' ); } // ------------------------------------------------------------------------- diff --git a/includes/Core/Deactivator.php b/includes/Core/Deactivator.php index a4897a9..f4f54c9 100644 --- a/includes/Core/Deactivator.php +++ b/includes/Core/Deactivator.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Core; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Queue\Scheduler; /** diff --git a/includes/Core/Plugin.php b/includes/Core/Plugin.php index bcd0a42..655cba8 100644 --- a/includes/Core/Plugin.php +++ b/includes/Core/Plugin.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Core; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\AlertsPage; use MediaRightsAudit\Admin\AttachmentDetailPage; use MediaRightsAudit\Admin\AuditPage; diff --git a/includes/Core/Queue/Scheduler.php b/includes/Core/Queue/Scheduler.php index dca07bc..69e3d58 100644 --- a/includes/Core/Queue/Scheduler.php +++ b/includes/Core/Queue/Scheduler.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Core\Queue; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Thin wrapper around Action Scheduler that namespaces all jobs under a single group. * diff --git a/includes/External/AbstractProvider.php b/includes/External/AbstractProvider.php index 938ab4e..11252c9 100644 --- a/includes/External/AbstractProvider.php +++ b/includes/External/AbstractProvider.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Provides rate-limited scanning via a transient-based per-minute counter. * diff --git a/includes/External/ExternalScanner.php b/includes/External/ExternalScanner.php index 3afccf6..03f59b2 100644 --- a/includes/External/ExternalScanner.php +++ b/includes/External/ExternalScanner.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Queue\Scheduler; /** @@ -145,6 +149,8 @@ class ExternalScanner { self::scan_batch( $batch_size ); + delete_transient( 'mra_alert_count' ); + if ( self::get_pending_count() > 0 ) { 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 + + delete_transient( 'mra_alert_count' ); } /** @@ -381,6 +389,8 @@ class ExternalScanner { ); // phpcs:enable WordPress.DB.DirectDatabaseQuery + delete_transient( 'mra_alert_count' ); + return $count; } diff --git a/includes/External/GoogleVisionProvider.php b/includes/External/GoogleVisionProvider.php index 3a68565..67204ae 100644 --- a/includes/External/GoogleVisionProvider.php +++ b/includes/External/GoogleVisionProvider.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\Settings; /** diff --git a/includes/External/HostnameFilter.php b/includes/External/HostnameFilter.php index e6dbcbb..761081b 100644 --- a/includes/External/HostnameFilter.php +++ b/includes/External/HostnameFilter.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Classifies hostnames against the site's configured alert (include) and * ignored (exclude) lists stored in the robotstxt_mediaaudit_settings option. diff --git a/includes/External/PicDefenseProvider.php b/includes/External/PicDefenseProvider.php index 6e0a808..36eb1c3 100644 --- a/includes/External/PicDefenseProvider.php +++ b/includes/External/PicDefenseProvider.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\Settings; /** diff --git a/includes/External/ResultsConsolidator.php b/includes/External/ResultsConsolidator.php index 2298952..a324381 100644 --- a/includes/External/ResultsConsolidator.php +++ b/includes/External/ResultsConsolidator.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Aggregates external scan results across multiple providers to surface * domains that appear in more than one provider's findings (consensus signals). diff --git a/includes/External/ScanResult.php b/includes/External/ScanResult.php index 3d64845..7925273 100644 --- a/includes/External/ScanResult.php +++ b/includes/External/ScanResult.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Immutable result returned by AbstractProvider::scan(). */ diff --git a/includes/External/TinEyeProvider.php b/includes/External/TinEyeProvider.php index 1caacc4..cc0b06e 100644 --- a/includes/External/TinEyeProvider.php +++ b/includes/External/TinEyeProvider.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\External; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Admin\Settings; /** diff --git a/includes/Internal/AttachmentIndexer.php b/includes/Internal/AttachmentIndexer.php index 4f213b0..7b6cec4 100644 --- a/includes/Internal/AttachmentIndexer.php +++ b/includes/Internal/AttachmentIndexer.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Internal; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Queue\Scheduler; /** diff --git a/includes/Internal/UsageScanner.php b/includes/Internal/UsageScanner.php index 470d859..616686e 100644 --- a/includes/Internal/UsageScanner.php +++ b/includes/Internal/UsageScanner.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Internal; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + use MediaRightsAudit\Core\Queue\Scheduler; /** diff --git a/includes/Privacy/DataEraser.php b/includes/Privacy/DataEraser.php index 5588485..ae19787 100644 --- a/includes/Privacy/DataEraser.php +++ b/includes/Privacy/DataEraser.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Privacy; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Erases Media Audit data for a given user via the WordPress privacy tools. * diff --git a/includes/Privacy/DataExporter.php b/includes/Privacy/DataExporter.php index 7dc7cb8..ce2d7d6 100644 --- a/includes/Privacy/DataExporter.php +++ b/includes/Privacy/DataExporter.php @@ -7,6 +7,10 @@ namespace MediaRightsAudit\Privacy; +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * Exports Media Audit data for a given user via the WordPress privacy tools. * diff --git a/readme.txt b/readme.txt index 802c8ef..51499af 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === Media Audit (by ROBOTSTXT) === Contributors: javiercasares, robotstxt Tags: media, copyright, images, reverse image search, media library -Requires at least: 6.8 +Requires at least: 5.3 Tested up to: 7.0 -Requires PHP: 8.2 +Requires PHP: 8.0 Requires Plugins: action-scheduler -Stable tag: 1.7.0 +Stable tag: 1.7.1 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/robotstxt-mediaaudit.php b/robotstxt-mediaaudit.php index 6652d90..d95337c 100644 --- a/robotstxt-mediaaudit.php +++ b/robotstxt-mediaaudit.php @@ -3,10 +3,10 @@ * Plugin Name: Media Audit (by ROBOTSTXT) * 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. - * Version: 1.7.0 - * Requires at least: 6.8 + * Version: 1.7.1 + * Requires at least: 5.3 * Tested up to: 7.0 - * Requires PHP: 8.2 + * Requires PHP: 8.0 * Requires Plugins: action-scheduler * Author: ROBOTSTXT * Author URI: https://www.robotstxt.es/ @@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) { 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_PLUGIN_FILE', __FILE__ ); define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); diff --git a/robotstxt-updater.php b/robotstxt-updater.php new file mode 100644 index 0000000..2e26d14 --- /dev/null +++ b/robotstxt-updater.php @@ -0,0 +1,383 @@ +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' ); + } + } +} diff --git a/vendor/autoload.php b/vendor/autoload.php index ebcfd49..bfed1dc 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,12 +14,9 @@ if (PHP_VERSION_ID < 50600) { echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2::getLoader(); +return ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757::getLoader(); diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 51e734a..2052022 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -26,12 +26,23 @@ use Composer\Semver\VersionParser; */ 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 * @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}|array{}|null */ private static $installed; + /** + * @var bool + */ + private static $installedIsLocalDir; + /** * @var bool|null */ @@ -309,6 +320,24 @@ class InstalledVersions { self::$installed = $data; 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(); + $copiedLocalDir = false; if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } 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} $required */ $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; - if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { - self::$installed = $installed[count($installed) - 1]; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + 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; } diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE index 62ecfd8..f27399a 100644 --- a/vendor/composer/LICENSE +++ b/vendor/composer/LICENSE @@ -1,3 +1,4 @@ + Copyright (c) Nils Adermann, Jordi Boggiano 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, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index b777424..ca8aac1 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2 +class ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757 { private static $loader; @@ -22,12 +22,14 @@ class ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2 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__)); - spl_autoload_unregister(array('ComposerAutoloaderInit957728ab3efa005f456e5f9df13a19d2', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit149e6f8eeaef8d46113f09e1e8d4e757', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::getInitializer($loader)); $loader->register(true); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index b7dcac9..700728e 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -4,17 +4,17 @@ namespace Composer\Autoload; -class ComposerStaticInit957728ab3efa005f456e5f9df13a19d2 +class ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757 { public static $prefixLengthsPsr4 = array ( - 'M' => + 'M' => array ( 'MediaRightsAudit\\' => 17, ), ); public static $prefixDirsPsr4 = array ( - 'MediaRightsAudit\\' => + 'MediaRightsAudit\\' => array ( 0 => __DIR__ . '/../..' . '/includes', ), @@ -51,9 +51,9 @@ class ComposerStaticInit957728ab3efa005f456e5f9df13a19d2 public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInit957728ab3efa005f456e5f9df13a19d2::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit149e6f8eeaef8d46113f09e1e8d4e757::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 446e747..2329daa 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,6 +1,6 @@ array( - 'name' => 'robotstxt/mediaaudit', + 'name' => 'robotstxt/robotstxt-mediaaudit', 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => null, @@ -10,7 +10,7 @@ 'dev' => false, ), 'versions' => array( - 'robotstxt/mediaaudit' => array( + 'robotstxt/robotstxt-mediaaudit' => array( 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => null, diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..14bf88d --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 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) + ); +}