v1.0.0
This commit is contained in:
commit
6708bbb67f
43 changed files with 8342 additions and 0 deletions
25
includes/Core/Activator.php
Normal file
25
includes/Core/Activator.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* Fired during plugin activation.
|
||||
*
|
||||
* @package MediaRightsAudit\Core
|
||||
*/
|
||||
|
||||
namespace MediaRightsAudit\Core;
|
||||
|
||||
/**
|
||||
* Handles tasks that run once when the plugin is activated.
|
||||
*/
|
||||
class Activator {
|
||||
|
||||
/**
|
||||
* Creates database tables and seeds the initial DB version.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate(): void {
|
||||
Database::create_tables();
|
||||
update_option( 'robotstxt_mediaaudit_db_version', ROBOTSTXT_MEDIAAUDIT_DB_VERSION );
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
141
includes/Core/Database.php
Normal file
141
includes/Core/Database.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
/**
|
||||
* Database schema creation and versioned migrations.
|
||||
*
|
||||
* @package MediaRightsAudit\Core
|
||||
*/
|
||||
|
||||
namespace MediaRightsAudit\Core;
|
||||
|
||||
/**
|
||||
* Manages all custom table creation and schema migrations.
|
||||
*
|
||||
* Migration methods are idempotent: they rely on dbDelta's ALTER TABLE
|
||||
* diffing and can be run multiple times without side effects.
|
||||
*/
|
||||
class Database {
|
||||
|
||||
/**
|
||||
* Creates all tables. Called on plugin activation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function create_tables(): void {
|
||||
self::migration_100();
|
||||
self::migration_101();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies any pending migrations based on the currently installed schema version.
|
||||
*
|
||||
* Called on admin_init by Plugin::check_db_version().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run_migrations(): void {
|
||||
$raw = get_option( 'robotstxt_mediaaudit_db_version', '0.0.0' );
|
||||
$current = is_string( $raw ) ? $raw : '0.0.0';
|
||||
|
||||
if ( version_compare( $current, '1.0.0', '<' ) ) {
|
||||
self::migration_100();
|
||||
}
|
||||
|
||||
if ( version_compare( $current, '1.0.1', '<' ) ) {
|
||||
self::migration_101();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all plugin tables. Used by uninstall.php.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function drop_tables(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_external_results`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_media_usage`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_media_index`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Migrations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Migration 1.0.0 — creates the initial three tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migration_100(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
dbDelta(
|
||||
"CREATE TABLE {$wpdb->prefix}mra_media_index (
|
||||
attachment_id bigint(20) unsigned NOT NULL,
|
||||
file_url text NOT NULL,
|
||||
file_name varchar(255) NOT NULL DEFAULT '',
|
||||
mime_type varchar(100) NOT NULL DEFAULT '',
|
||||
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
internal_scanned_at datetime DEFAULT NULL,
|
||||
external_status enum('pending','queued','scanned','matches','error') NOT NULL DEFAULT 'pending',
|
||||
external_scanned_at datetime DEFAULT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (attachment_id),
|
||||
KEY external_status (external_status)
|
||||
) {$charset_collate};"
|
||||
);
|
||||
|
||||
dbDelta(
|
||||
"CREATE TABLE {$wpdb->prefix}mra_media_usage (
|
||||
usage_id bigint(20) unsigned NOT NULL auto_increment,
|
||||
attachment_id bigint(20) unsigned NOT NULL,
|
||||
post_id bigint(20) unsigned NOT NULL,
|
||||
post_type varchar(20) NOT NULL DEFAULT '',
|
||||
context enum('featured','content','meta') NOT NULL DEFAULT 'featured',
|
||||
meta_key varchar(255) DEFAULT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (usage_id),
|
||||
KEY attachment_id (attachment_id),
|
||||
KEY post_id (post_id)
|
||||
) {$charset_collate};"
|
||||
);
|
||||
|
||||
dbDelta(
|
||||
"CREATE TABLE {$wpdb->prefix}mra_external_results (
|
||||
result_id bigint(20) unsigned NOT NULL auto_increment,
|
||||
attachment_id bigint(20) unsigned NOT NULL,
|
||||
provider enum('google_vision','tineye') NOT NULL,
|
||||
raw_response json DEFAULT NULL,
|
||||
match_count int(10) unsigned NOT NULL DEFAULT 0,
|
||||
top_domains json DEFAULT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (result_id),
|
||||
UNIQUE KEY attachment_provider (attachment_id,provider)
|
||||
) {$charset_collate};"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration 1.0.1 — adds 'queued' to the external_status ENUM.
|
||||
*
|
||||
* Uses ALTER TABLE directly because dbDelta cannot modify ENUM column definitions.
|
||||
* Safe to re-run: MariaDB/MySQL treats an identical MODIFY as a no-op.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migration_101(): void {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
|
||||
$wpdb->query(
|
||||
"ALTER TABLE `{$wpdb->prefix}mra_media_index`
|
||||
MODIFY COLUMN external_status
|
||||
enum('pending','queued','scanned','matches','error') NOT NULL DEFAULT 'pending'"
|
||||
);
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
|
||||
}
|
||||
}
|
||||
28
includes/Core/Deactivator.php
Normal file
28
includes/Core/Deactivator.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/**
|
||||
* Fired during plugin deactivation.
|
||||
*
|
||||
* @package MediaRightsAudit\Core
|
||||
*/
|
||||
|
||||
namespace MediaRightsAudit\Core;
|
||||
|
||||
use MediaRightsAudit\Core\Queue\Scheduler;
|
||||
|
||||
/**
|
||||
* Handles tasks that run once when the plugin is deactivated.
|
||||
*/
|
||||
class Deactivator {
|
||||
|
||||
/**
|
||||
* Cancels all pending scheduled actions and flushes rewrite rules.
|
||||
*
|
||||
* Database tables and stored data are intentionally preserved.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate(): void {
|
||||
Scheduler::cancel_all();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
165
includes/Core/Plugin.php
Normal file
165
includes/Core/Plugin.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
/**
|
||||
* Core plugin bootstrap class.
|
||||
*
|
||||
* @package MediaRightsAudit\Core
|
||||
*/
|
||||
|
||||
namespace MediaRightsAudit\Core;
|
||||
|
||||
use MediaRightsAudit\Admin\AuditPage;
|
||||
use MediaRightsAudit\Admin\Settings;
|
||||
use MediaRightsAudit\CLI\Command;
|
||||
use MediaRightsAudit\External\AbstractProvider;
|
||||
use MediaRightsAudit\External\ExternalScanner;
|
||||
use MediaRightsAudit\External\GoogleVisionProvider;
|
||||
use MediaRightsAudit\External\TinEyeProvider;
|
||||
use MediaRightsAudit\Internal\AttachmentIndexer;
|
||||
use MediaRightsAudit\Internal\UsageScanner;
|
||||
use MediaRightsAudit\Privacy\DataEraser;
|
||||
use MediaRightsAudit\Privacy\DataExporter;
|
||||
|
||||
/**
|
||||
* Registers all hooks and initialises the plugin subsystems.
|
||||
*/
|
||||
class Plugin {
|
||||
|
||||
/**
|
||||
* Settings page handler.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* Audit list page controller.
|
||||
*
|
||||
* @var AuditPage
|
||||
*/
|
||||
private AuditPage $audit_page;
|
||||
|
||||
/**
|
||||
* Initialises dependencies.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->settings = new Settings();
|
||||
$this->audit_page = new AuditPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches all WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void {
|
||||
add_action( 'init', array( $this, 'load_textdomain' ) );
|
||||
add_action( 'admin_menu', array( $this, 'register_admin_pages' ) );
|
||||
add_action( 'admin_init', array( $this->settings, 'register' ) );
|
||||
add_action( 'admin_init', array( $this, 'check_db_version' ) );
|
||||
add_action( AttachmentIndexer::AS_HOOK, array( AttachmentIndexer::class, 'process_scheduled_batch' ) );
|
||||
add_action( UsageScanner::AS_HOOK, array( UsageScanner::class, 'process_scheduled_batch' ) );
|
||||
add_action( ExternalScanner::AS_HOOK, array( ExternalScanner::class, 'process_scheduled_batch' ) );
|
||||
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' ) );
|
||||
$this->audit_page->register_hooks();
|
||||
$this->register_cli_commands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the plugin text domain.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load_textdomain(): void {
|
||||
load_plugin_textdomain(
|
||||
'robotstxt-mediaaudit',
|
||||
false,
|
||||
dirname( plugin_basename( ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE ) ) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers admin menu pages.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_admin_pages(): void {
|
||||
$suffix = add_menu_page(
|
||||
__( 'Media Audit', 'robotstxt-mediaaudit' ),
|
||||
__( 'Media Audit', 'robotstxt-mediaaudit' ),
|
||||
'edit_others_posts',
|
||||
'robotstxt-mediaaudit',
|
||||
array( $this->audit_page, 'render' ),
|
||||
'dashicons-camera',
|
||||
60
|
||||
);
|
||||
|
||||
$this->audit_page->set_hook_suffix( $suffix );
|
||||
|
||||
// Rename the auto-created first submenu entry.
|
||||
add_submenu_page(
|
||||
'robotstxt-mediaaudit',
|
||||
__( 'Media Audit', 'robotstxt-mediaaudit' ),
|
||||
__( 'Media Audit', 'robotstxt-mediaaudit' ),
|
||||
'edit_others_posts',
|
||||
'robotstxt-mediaaudit',
|
||||
array( $this->audit_page, 'render' )
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'robotstxt-mediaaudit',
|
||||
__( 'Settings', 'robotstxt-mediaaudit' ),
|
||||
__( 'Settings', 'robotstxt-mediaaudit' ),
|
||||
'manage_options',
|
||||
'robotstxt-mediaaudit-settings',
|
||||
array( $this->settings, 'render' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs pending DB migrations when the stored schema version is behind the constant.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_db_version(): void {
|
||||
$raw = get_option( 'robotstxt_mediaaudit_db_version', '0.0.0' );
|
||||
$stored = is_string( $raw ) ? $raw : '0.0.0';
|
||||
if ( ROBOTSTXT_MEDIAAUDIT_DB_VERSION !== $stored ) {
|
||||
Database::run_migrations();
|
||||
update_option( 'robotstxt_mediaaudit_db_version', ROBOTSTXT_MEDIAAUDIT_DB_VERSION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers built-in external providers via the mra/external/providers filter.
|
||||
*
|
||||
* @param mixed $providers Accumulated provider list.
|
||||
*
|
||||
* @return array<\MediaRightsAudit\External\AbstractProvider>
|
||||
*/
|
||||
public function register_providers( $providers ): array {
|
||||
$list = array();
|
||||
if ( is_array( $providers ) ) {
|
||||
foreach ( $providers as $p ) {
|
||||
if ( $p instanceof AbstractProvider ) {
|
||||
$list[] = $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
$list[] = new GoogleVisionProvider();
|
||||
$list[] = new TinEyeProvider();
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers WP-CLI commands when running in CLI context.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_cli_commands(): void {
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
\WP_CLI::add_command( 'mra', Command::class );
|
||||
}
|
||||
}
|
||||
}
|
||||
77
includes/Core/Queue/Scheduler.php
Normal file
77
includes/Core/Queue/Scheduler.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/**
|
||||
* Action Scheduler wrapper.
|
||||
*
|
||||
* @package MediaRightsAudit\Core\Queue
|
||||
*/
|
||||
|
||||
namespace MediaRightsAudit\Core\Queue;
|
||||
|
||||
/**
|
||||
* Thin wrapper around Action Scheduler that namespaces all jobs under a single group.
|
||||
*
|
||||
* Action Scheduler is declared as a required plugin (Requires Plugins: action-scheduler)
|
||||
* so its functions are always available at runtime. The is_available() guard is kept
|
||||
* for defensive unit-test scenarios only.
|
||||
*/
|
||||
class Scheduler {
|
||||
|
||||
/**
|
||||
* Action Scheduler group for all plugin jobs.
|
||||
*/
|
||||
const GROUP = 'robotstxt-mediaaudit';
|
||||
|
||||
/**
|
||||
* Returns true when Action Scheduler functions are available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_available(): bool {
|
||||
return function_exists( 'as_schedule_single_action' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a single action to run after the given delay.
|
||||
*
|
||||
* @param string $hook Action hook name.
|
||||
* @param array<mixed> $args Arguments passed to the hook.
|
||||
* @param int $delay Seconds from now (default 0 = immediate).
|
||||
*
|
||||
* @return int|null Action Scheduler action ID, or null if unavailable.
|
||||
*/
|
||||
public static function schedule_single( string $hook, array $args = array(), int $delay = 0 ): ?int {
|
||||
if ( ! self::is_available() ) {
|
||||
return null;
|
||||
}
|
||||
return as_schedule_single_action( time() + $delay, $hook, $args, self::GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a pending (not-yet-run) action exists for this hook.
|
||||
*
|
||||
* @param string $hook Action hook name.
|
||||
* @param array<mixed> $args Arguments to match.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_pending( string $hook, array $args = array() ): bool {
|
||||
if ( ! self::is_available() ) {
|
||||
return false;
|
||||
}
|
||||
return as_has_scheduled_action( $hook, $args, self::GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all pending plugin actions across all hooks.
|
||||
*
|
||||
* Called on plugin deactivation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cancel_all(): void {
|
||||
if ( ! self::is_available() ) {
|
||||
return;
|
||||
}
|
||||
as_unschedule_all_actions( '', array(), self::GROUP );
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue