This commit is contained in:
Javier Casares 2026-06-03 06:24:03 +00:00
commit 6708bbb67f
43 changed files with 8342 additions and 0 deletions

View file

@ -0,0 +1,316 @@
<?php
/**
* GDPR personal data exporter.
*
* @package MediaRightsAudit\Privacy
*/
namespace MediaRightsAudit\Privacy;
/**
* Exports Media Audit data for a given user via the WordPress privacy tools.
*
* Registered via the wp_privacy_personal_data_exporters filter. Exports rows
* from mra_media_index and mra_external_results for all attachments uploaded
* by the requested user. Results are paginated at 20 attachments per page.
*/
class DataExporter {
/**
* Group ID for media index export items.
*/
const GROUP_INDEX = 'mra-media-index';
/**
* Group ID for external results export items.
*/
const GROUP_RESULTS = 'mra-external-results';
/**
* Number of attachments processed per export page.
*/
const PER_PAGE = 20;
/**
* Registers this exporter with the WordPress privacy API.
*
* Hooked to wp_privacy_personal_data_exporters.
*
* @param array<string, mixed> $exporters Accumulated exporter list.
*
* @return array<string, mixed>
*/
public static function register( array $exporters ): array {
$exporters['robotstxt-mediaaudit'] = array(
'exporter_friendly_name' => __( 'Media Audit', 'robotstxt-mediaaudit' ),
'callback' => array( self::class, 'export' ),
);
return $exporters;
}
/**
* Exports Media Audit data for the user identified by $email_address.
*
* @param string $email_address User e-mail address.
* @param int $page Current pagination page (1-based).
*
* @return array{data: array<mixed>, done: bool}
*/
public static function export( string $email_address, int $page = 1 ): array {
$user = get_user_by( 'email', $email_address );
if ( ! $user instanceof \WP_User ) {
return array(
'data' => array(),
'done' => true,
);
}
$offset = ( $page - 1 ) * self::PER_PAGE;
$attachment_ids = self::get_attachment_ids( $user->ID, $offset, self::PER_PAGE );
if ( empty( $attachment_ids ) ) {
return array(
'data' => array(),
'done' => true,
);
}
$data = array();
foreach ( self::get_index_rows( $attachment_ids ) as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$aid_val = $row['attachment_id'] ?? null;
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
$data[] = array(
'group_id' => self::GROUP_INDEX,
'group_label' => __( 'Media Audit Indexed Files', 'robotstxt-mediaaudit' ),
'item_id' => 'mra-index-' . $aid,
'data' => self::format_index_row( $row, $aid ),
);
}
foreach ( self::get_result_rows( $attachment_ids ) as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$aid_val = $row['attachment_id'] ?? null;
$aid = is_numeric( $aid_val ) ? (int) $aid_val : 0;
$provider_val = $row['provider'] ?? null;
$provider = is_string( $provider_val ) ? $provider_val : '';
$data[] = array(
'group_id' => self::GROUP_RESULTS,
'group_label' => __( 'Media Audit External Scan Results', 'robotstxt-mediaaudit' ),
'item_id' => 'mra-result-' . $aid . '-' . $provider,
'data' => self::format_result_row( $row, $aid, $provider ),
);
}
$total = self::count_attachments( $user->ID );
$done = ( $page * self::PER_PAGE ) >= $total;
return array(
'data' => $data,
'done' => $done,
);
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/**
* Returns attachment IDs from the plugin index for a given user, paginated.
*
* @param int $user_id WordPress user ID.
* @param int $offset Number of rows to skip.
* @param int $limit Maximum number of rows to return.
*
* @return array<int, int>
*/
private static function get_attachment_ids( int $user_id, int $offset, int $limit ): 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
LIMIT %d OFFSET %d",
$user_id,
$limit,
$offset
)
);
// 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 );
}
/**
* Counts all indexed attachments for a given user.
*
* @param int $user_id WordPress user ID.
*
* @return int
*/
private static function count_attachments( int $user_id ): int {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*)
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",
$user_id
)
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return is_numeric( $result ) ? (int) $result : 0;
}
/**
* Fetches index rows for the given attachment IDs.
*
* @param array<int, int> $ids Attachment IDs.
*
* @return array<mixed>
*/
private static function get_index_rows( array $ids ): array {
global $wpdb;
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT attachment_id, file_name, file_url, mime_type, external_status,
internal_scanned_at, external_scanned_at
FROM {$wpdb->prefix}mra_media_index
WHERE attachment_id IN ({$placeholders})",
...$ids
),
ARRAY_A
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return is_array( $rows ) ? $rows : array();
}
/**
* Fetches external result rows for the given attachment IDs.
*
* @param array<int, int> $ids Attachment IDs.
*
* @return array<mixed>
*/
private static function get_result_rows( array $ids ): array {
global $wpdb;
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT attachment_id, provider, match_count, created_at
FROM {$wpdb->prefix}mra_external_results
WHERE attachment_id IN ({$placeholders})",
...$ids
),
ARRAY_A
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return is_array( $rows ) ? $rows : array();
}
/**
* Formats a mra_media_index row into WordPress privacy export data items.
*
* @param array<mixed, mixed> $row DB row.
* @param int $aid Attachment ID.
*
* @return array<int, array<string, string>>
*/
private static function format_index_row( array $row, int $aid ): array {
$file_name = is_string( $row['file_name'] ?? null ) ? $row['file_name'] : '';
$file_url = is_string( $row['file_url'] ?? null ) ? $row['file_url'] : '';
$status = is_string( $row['external_status'] ?? null ) ? $row['external_status'] : '';
$int_at = is_string( $row['internal_scanned_at'] ?? null ) ? $row['internal_scanned_at'] : '';
$ext_at = is_string( $row['external_scanned_at'] ?? null ) ? $row['external_scanned_at'] : '';
return array(
array(
'name' => __( 'Attachment ID', 'robotstxt-mediaaudit' ),
'value' => (string) $aid,
),
array(
'name' => __( 'Filename', 'robotstxt-mediaaudit' ),
'value' => $file_name,
),
array(
'name' => __( 'File URL', 'robotstxt-mediaaudit' ),
'value' => $file_url,
),
array(
'name' => __( 'External Scan Status', 'robotstxt-mediaaudit' ),
'value' => $status,
),
array(
'name' => __( 'Last Scanned (internal)', 'robotstxt-mediaaudit' ),
'value' => $int_at,
),
array(
'name' => __( 'Last Scanned (external)', 'robotstxt-mediaaudit' ),
'value' => $ext_at,
),
);
}
/**
* Formats a mra_external_results row into WordPress privacy export data items.
*
* @param array<mixed, mixed> $row DB row.
* @param int $aid Attachment ID.
* @param string $provider Provider slug.
*
* @return array<int, array<string, string>>
*/
private static function format_result_row( array $row, int $aid, string $provider ): array {
$mc_val = $row['match_count'] ?? null;
$match_count = is_numeric( $mc_val ) ? (int) $mc_val : 0;
$created_at = is_string( $row['created_at'] ?? null ) ? $row['created_at'] : '';
return array(
array(
'name' => __( 'Attachment ID', 'robotstxt-mediaaudit' ),
'value' => (string) $aid,
),
array(
'name' => __( 'Provider', 'robotstxt-mediaaudit' ),
'value' => $provider,
),
array(
'name' => __( 'Match Count', 'robotstxt-mediaaudit' ),
'value' => (string) $match_count,
),
array(
'name' => __( 'Scanned At', 'robotstxt-mediaaudit' ),
'value' => $created_at,
),
);
}
}