robotstxt-mediaaudit/includes/Core/Database.php
2026-06-03 06:30:49 +00:00

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};"
);
}
}