robotstxt-mediaaudit/includes/Privacy/DataEraser.php
2026-06-03 06:24:03 +00:00

160 lines
4.8 KiB
PHP

<?php
/**
* GDPR personal data eraser.
*
* @package MediaRightsAudit\Privacy
*/
namespace MediaRightsAudit\Privacy;
/**
* Erases Media Audit data for a given user via the WordPress privacy tools.
*
* Registered via the wp_privacy_personal_data_erasers filter. Removes all rows
* from mra_media_index, mra_media_usage, and mra_external_results that belong
* to attachments uploaded by the requested user.
*
* Erasure is a hard delete: once removed, the data cannot be recovered. The
* attachment itself (wp_posts row) is not touched — only plugin-specific data.
*/
class DataEraser {
/**
* Registers this eraser with the WordPress privacy API.
*
* Hooked to wp_privacy_personal_data_erasers.
*
* @param array<string, mixed> $erasers Accumulated eraser list.
*
* @return array<string, mixed>
*/
public static function register( array $erasers ): array {
$erasers['robotstxt-mediaaudit'] = array(
'eraser_friendly_name' => __( 'Media Audit', 'robotstxt-mediaaudit' ),
'callback' => array( self::class, 'erase' ),
);
return $erasers;
}
/**
* Erases Media Audit data for the user identified by $email_address.
*
* @param string $email_address User e-mail address.
* @param int $page Pagination page (unused; all data erased in one pass).
*
* @return array{items_removed: int, items_retained: int, messages: array<mixed>, done: bool}
*/
public static function erase( string $email_address, int $page = 1 ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by WP privacy API signature
$user = get_user_by( 'email', $email_address );
if ( ! $user instanceof \WP_User ) {
return self::done_response( 0 );
}
$attachment_ids = self::get_attachment_ids( $user->ID );
if ( empty( $attachment_ids ) ) {
return self::done_response( 0 );
}
$removed = self::delete_plugin_data( $attachment_ids );
return self::done_response( $removed );
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/**
* Builds a standard "done" response array.
*
* @param int $removed Number of records removed.
*
* @return array{items_removed: int, items_retained: int, messages: array<mixed>, done: bool}
*/
private static function done_response( int $removed ): array {
return array(
'items_removed' => $removed,
'items_retained' => 0,
'messages' => array(),
'done' => true,
);
}
/**
* Returns all indexed attachment IDs for a given user (no pagination).
*
* @param int $user_id WordPress user ID.
*
* @return array<int, int>
*/
private static function get_attachment_ids( int $user_id ): array {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$raw = $wpdb->get_col(
$wpdb->prepare(
"SELECT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->prefix}mra_media_index mi ON p.ID = mi.attachment_id
WHERE p.post_type = 'attachment'
AND p.post_author = %d
ORDER BY p.ID ASC",
$user_id
)
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! is_array( $raw ) ) {
return array();
}
return array_map( static fn ( mixed $v ): int => is_numeric( $v ) ? (int) $v : 0, $raw );
}
/**
* Deletes all plugin data rows for the given attachment IDs.
*
* Removes records from mra_external_results, mra_media_usage, and
* mra_media_index. Returns the total number of affected rows.
*
* @param array<int, int> $ids Attachment IDs to delete.
*
* @return int Total rows deleted across all three tables.
*/
private static function delete_plugin_data( array $ids ): int {
global $wpdb;
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$removed = 0;
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}mra_external_results WHERE attachment_id IN ({$placeholders})",
...$ids
)
);
$removed += (int) $wpdb->rows_affected;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}mra_media_usage WHERE attachment_id IN ({$placeholders})",
...$ids
)
);
$removed += (int) $wpdb->rows_affected;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}mra_media_index WHERE attachment_id IN ({$placeholders})",
...$ids
)
);
$removed += (int) $wpdb->rows_affected;
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return $removed;
}
}