290 lines
8.7 KiB
PHP
290 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* Migration log manager.
|
|
*
|
|
* Handles creation, insertion, retrieval, truncation, and CSV export
|
|
* of the migration log table.
|
|
*
|
|
* @package Robotstxt_Mlp2_Mlp5
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class Robotstxt_Mlp2_Mlp5_Logger
|
|
*
|
|
* All queries in this class are intentionally direct — the log table is
|
|
* write-once per operation and must never be read from an object cache.
|
|
* Per-line DirectDatabaseQuery suppressions are applied throughout.
|
|
*/
|
|
class Robotstxt_Mlp2_Mlp5_Logger {
|
|
|
|
/**
|
|
* Sitemeta key used to track the installed DB schema version.
|
|
*
|
|
* @var string
|
|
*/
|
|
const DB_VERSION_KEY = 'mlp_migrator_db_version';
|
|
|
|
/**
|
|
* Return the fully qualified log table name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function table_name(): string {
|
|
global $wpdb;
|
|
return $wpdb->base_prefix . 'mlp_migrator_log';
|
|
}
|
|
|
|
/**
|
|
* Create (or update) the log table via dbDelta().
|
|
*
|
|
* Safe to call multiple times — dbDelta() is idempotent and only alters
|
|
* the table when the schema has changed. The DB upgrader is responsible
|
|
* for calling this method and for updating the stored schema version.
|
|
*/
|
|
public static function create_table(): void {
|
|
global $wpdb;
|
|
|
|
$table = self::table_name();
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
// Note: dbDelta() requires two spaces before PRIMARY KEY.
|
|
$sql = "CREATE TABLE {$table} (
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
created_at datetime NOT NULL,
|
|
type varchar(20) NOT NULL,
|
|
context varchar(50) NOT NULL,
|
|
source_id bigint(20) unsigned DEFAULT NULL,
|
|
target_id bigint(20) unsigned DEFAULT NULL,
|
|
message text NOT NULL,
|
|
metadata longtext DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY type (type),
|
|
KEY context (context)
|
|
) {$charset_collate};";
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
dbDelta( $sql );
|
|
}
|
|
|
|
/**
|
|
* Drop the log table and remove the schema version option.
|
|
*
|
|
* Only called from uninstall.php when the user has explicitly opted in
|
|
* to data deletion via the plugin settings.
|
|
*/
|
|
public static function drop_table(): void {
|
|
global $wpdb;
|
|
|
|
$table = self::table_name();
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
|
|
|
|
delete_site_option( self::DB_VERSION_KEY );
|
|
}
|
|
|
|
/**
|
|
* Insert a log entry.
|
|
*
|
|
* @param string $type Entry type: 'success', 'skipped', or 'error'.
|
|
* @param string $context Operation context: 'site_relation', 'post_relation', 'term_relation', or 'cleanup'.
|
|
* @param int|null $source_id Source content or site ID, or null if not applicable.
|
|
* @param int|null $target_id Target content or site ID, or null if not applicable.
|
|
* @param string $message Human-readable description of the operation result.
|
|
* @param array $metadata Optional additional structured data (stored as JSON).
|
|
*
|
|
* @return int Inserted row ID, or 0 on failure.
|
|
*/
|
|
public static function log(
|
|
string $type,
|
|
string $context,
|
|
?int $source_id,
|
|
?int $target_id,
|
|
string $message,
|
|
array $metadata = array()
|
|
): int {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
|
$inserted = $wpdb->insert(
|
|
self::table_name(),
|
|
array(
|
|
'created_at' => current_time( 'mysql', true ),
|
|
'type' => $type,
|
|
'context' => $context,
|
|
'source_id' => $source_id,
|
|
'target_id' => $target_id,
|
|
'message' => $message,
|
|
'metadata' => empty( $metadata ) ? null : wp_json_encode( $metadata ),
|
|
),
|
|
array( '%s', '%s', '%s', '%d', '%d', '%s', '%s' )
|
|
);
|
|
|
|
return $inserted ? (int) $wpdb->insert_id : 0;
|
|
}
|
|
|
|
/**
|
|
* Retrieve log entries.
|
|
*
|
|
* @param array $args {
|
|
* Optional. Query arguments.
|
|
*
|
|
* @type string $type Filter by type: 'success', 'skipped', or 'error'.
|
|
* @type string $context Filter by context: 'site_relation', 'post_relation', 'term_relation', or 'cleanup'.
|
|
* @type int $limit Maximum number of rows to return. Default 50.
|
|
* @type int $offset Row offset for pagination. Default 0.
|
|
* @type string $order Sort direction: 'ASC' or 'DESC'. Default 'DESC'.
|
|
* }
|
|
*
|
|
* @return array Array of row objects, or an empty array.
|
|
*/
|
|
public static function get_entries( array $args = array() ): array {
|
|
global $wpdb;
|
|
|
|
$args = wp_parse_args(
|
|
$args,
|
|
array(
|
|
'type' => '',
|
|
'context' => '',
|
|
'limit' => 50,
|
|
'offset' => 0,
|
|
'order' => 'DESC',
|
|
)
|
|
);
|
|
|
|
$table = self::table_name();
|
|
$order = 'ASC' === strtoupper( (string) $args['order'] ) ? 'ASC' : 'DESC';
|
|
$where = '';
|
|
$values = array();
|
|
|
|
if ( ! empty( $args['type'] ) ) {
|
|
$where .= ' AND type = %s';
|
|
$values[] = $args['type'];
|
|
}
|
|
|
|
if ( ! empty( $args['context'] ) ) {
|
|
$where .= ' AND context = %s';
|
|
$values[] = $args['context'];
|
|
}
|
|
|
|
$values[] = (int) $args['limit'];
|
|
$values[] = (int) $args['offset'];
|
|
|
|
// Table name and ORDER BY direction are hardcoded — not from user input.
|
|
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
|
return (array) $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$table} WHERE 1=1{$where} ORDER BY id {$order} LIMIT %d OFFSET %d",
|
|
$values
|
|
)
|
|
);
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* Count log entries.
|
|
*
|
|
* Accepts the same filter arguments as get_entries() except limit, offset, and order.
|
|
*
|
|
* @param array $args Optional. 'type' and/or 'context' filter keys.
|
|
*
|
|
* @return int Total count of matching entries.
|
|
*/
|
|
public static function count( array $args = array() ): int {
|
|
global $wpdb;
|
|
|
|
$table = self::table_name();
|
|
$where = '';
|
|
$values = array();
|
|
|
|
if ( ! empty( $args['type'] ) ) {
|
|
$where .= ' AND type = %s';
|
|
$values[] = $args['type'];
|
|
}
|
|
|
|
if ( ! empty( $args['context'] ) ) {
|
|
$where .= ' AND context = %s';
|
|
$values[] = $args['context'];
|
|
}
|
|
|
|
if ( empty( $values ) ) {
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
|
|
}
|
|
|
|
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
|
return (int) $wpdb->get_var(
|
|
$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE 1=1{$where}", $values )
|
|
);
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* Truncate the log table.
|
|
*
|
|
* Removes all rows without dropping the table structure. Used when
|
|
* restarting a migration from scratch.
|
|
*/
|
|
public static function clear(): void {
|
|
global $wpdb;
|
|
|
|
$table = self::table_name();
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query( "TRUNCATE TABLE {$table}" );
|
|
}
|
|
|
|
/**
|
|
* Export the full log as a CSV string.
|
|
*
|
|
* Columns: id, created_at, type, context, source_id, target_id, message, metadata.
|
|
* Rows are ordered by id ASC.
|
|
*
|
|
* @return string CSV content with a header row, or an empty string on failure.
|
|
*/
|
|
public static function export_csv(): string {
|
|
global $wpdb;
|
|
|
|
$table = self::table_name();
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$rows = $wpdb->get_results( "SELECT * FROM {$table} ORDER BY id ASC", ARRAY_A );
|
|
|
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
|
$buffer = fopen( 'php://temp', 'r+' );
|
|
|
|
if ( false === $buffer ) {
|
|
return '';
|
|
}
|
|
|
|
fputcsv( $buffer, array( 'id', 'created_at', 'type', 'context', 'source_id', 'target_id', 'message', 'metadata' ) );
|
|
|
|
if ( ! empty( $rows ) ) {
|
|
foreach ( $rows as $row ) {
|
|
fputcsv(
|
|
$buffer,
|
|
array(
|
|
$row['id'],
|
|
$row['created_at'],
|
|
$row['type'],
|
|
$row['context'],
|
|
$row['source_id'] ?? '',
|
|
$row['target_id'] ?? '',
|
|
$row['message'],
|
|
$row['metadata'] ?? '',
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
rewind( $buffer );
|
|
$csv = stream_get_contents( $buffer );
|
|
fclose( $buffer ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
|
|
|
return is_string( $csv ) ? $csv : '';
|
|
}
|
|
}
|