v1.0.0
This commit is contained in:
commit
6708bbb67f
43 changed files with 8342 additions and 0 deletions
160
includes/Privacy/DataEraser.php
Normal file
160
includes/Privacy/DataEraser.php
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue