253 lines
8.2 KiB
PHP
253 lines
8.2 KiB
PHP
<?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();
|
|
self::migration_110();
|
|
self::migration_120();
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
if ( version_compare( $current, '1.1.0', '<' ) ) {
|
|
self::migration_110();
|
|
}
|
|
|
|
if ( version_compare( $current, '1.2.0', '<' ) ) {
|
|
self::migration_120();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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_dismissed_alerts`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_provider_status`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$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
|
|
}
|
|
|
|
/**
|
|
* Truncates all plugin tables, removing all data while preserving the table structure.
|
|
*
|
|
* Used by the Tools page "Reset all data" operation.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function truncate_all(): void {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_dismissed_alerts`" );
|
|
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_provider_status`" );
|
|
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_external_results`" );
|
|
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_usage`" );
|
|
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_index`" );
|
|
// phpcs:enable 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 varchar(100) 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
|
|
}
|
|
|
|
/**
|
|
* Migration 1.1.0 — widens provider column and adds mra_provider_status table.
|
|
*
|
|
* Changes:
|
|
* - ALTER mra_external_results.provider from ENUM to varchar(100) for extensibility.
|
|
* - CREATE mra_provider_status for per-provider scan tracking.
|
|
* - Backfill mra_provider_status from existing mra_external_results data.
|
|
*
|
|
* Safe to re-run: ALTER TABLE is a no-op if already varchar(100);
|
|
* dbDelta skips existing tables; INSERT IGNORE skips duplicate rows.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function migration_110(): void {
|
|
global $wpdb;
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
|
|
$wpdb->query(
|
|
"ALTER TABLE `{$wpdb->prefix}mra_external_results`
|
|
MODIFY COLUMN provider varchar(100) NOT NULL"
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
|
|
dbDelta(
|
|
"CREATE TABLE {$wpdb->prefix}mra_provider_status (
|
|
attachment_id bigint(20) unsigned NOT NULL,
|
|
provider varchar(100) NOT NULL,
|
|
status enum('pending','queued','scanned','error') NOT NULL DEFAULT 'pending',
|
|
scanned_at datetime DEFAULT NULL,
|
|
PRIMARY KEY (attachment_id,provider),
|
|
KEY status (status)
|
|
) {$charset_collate};"
|
|
);
|
|
|
|
// Backfill from existing data. Idempotent: INSERT IGNORE skips duplicates.
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query(
|
|
"INSERT IGNORE INTO {$wpdb->prefix}mra_provider_status (attachment_id, provider, status, scanned_at)
|
|
SELECT er.attachment_id, er.provider,
|
|
CASE WHEN mi.external_status = 'error' THEN 'error'
|
|
WHEN mi.external_status = 'queued' THEN 'queued'
|
|
ELSE 'scanned' END,
|
|
mi.external_scanned_at
|
|
FROM {$wpdb->prefix}mra_external_results er
|
|
JOIN {$wpdb->prefix}mra_media_index mi ON mi.attachment_id = er.attachment_id"
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
}
|
|
|
|
/**
|
|
* Migration 1.2.0 — creates the mra_dismissed_alerts table.
|
|
*
|
|
* Stores per-attachment alert dismissal records with dismisser, timestamp,
|
|
* and optional notes. Primary key on attachment_id (one dismissal per image).
|
|
*
|
|
* Safe to re-run: dbDelta skips existing tables.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function migration_120(): void {
|
|
global $wpdb;
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
|
|
dbDelta(
|
|
"CREATE TABLE {$wpdb->prefix}mra_dismissed_alerts (
|
|
attachment_id bigint(20) unsigned NOT NULL,
|
|
dismissed_by bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
dismissed_at datetime NOT NULL,
|
|
notes text NOT NULL DEFAULT '',
|
|
PRIMARY KEY (attachment_id)
|
|
) {$charset_collate};"
|
|
);
|
|
}
|
|
}
|