Export Personal Data. The log holds no personal * data itself; the records are found through the consent IDs kept in the user's meta (docs/subsystems.md ยง5.5). */ final class Exporter { public const GROUP = 'consentfold-consents'; /** * Prefix of the transients that carry the last row id sent from one page of an export to the next, one per user * and page, so a retried page or two exports of one user read the same cursor. */ private const MARKER = 'consentfold_export_'; /** * Log storage. * * @var LogRepository */ private LogRepository $log; /** * Constructor. * * @param LogRepository $log Log storage. */ public function __construct( LogRepository $log ) { $this->log = $log; } /** * Hook the exporter. */ public function register(): void { add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'add' ) ); } /** * Add the exporter to core's list. * * @param mixed $exporters Registered exporters. * @return array */ public function add( $exporters ): array { $exporters = is_array( $exporters ) ? $exporters : array(); $exporters[] = array( 'exporter_friendly_name' => __( 'Consent log', 'consentfold' ), 'callback' => array( $this, 'export' ), ); return $exporters; } /** * Export the records of the user with an email address. * * @param string $email Email address. * @param int $page Page of the export, from 1; core asks again until `done`. Page 1 starts fresh; later pages * continue after the last row id the previous page kept for an hour; a lost cursor falls back to * skipping `(page - 1) * PRIVACY_PAGE` records, so the export always ends. * @return array{data: list>, done: bool} */ public function export( string $email, int $page = 1 ): array { $user = get_user_by( 'email', $email ); $data = array(); $done = true; if ( $user instanceof \WP_User ) { $marker = self::MARKER . $user->ID . '_'; $after = $page > 1 ? (int) get_transient( $marker . $page ) : 0; $offset = $page > 1 && 0 === $after ? ( $page - 1 ) * LogRepository::PRIVACY_PAGE : 0; $result = $this->log->history_page( UserConsentIds::get( $user->ID ), $after, $offset ); $done = $result['done']; if ( $page > 1 ) { delete_transient( $marker . $page ); } if ( ! $done && array() !== $result['records'] ) { set_transient( $marker . ( $page + 1 ), end( $result['records'] )['id'], HOUR_IN_SECONDS ); } foreach ( $result['records'] as $record ) { $data[] = array( 'group_id' => self::GROUP, 'group_label' => __( 'Consent log', 'consentfold' ), 'item_id' => 'consentfold-' . $record['id'], 'data' => array( array( 'name' => __( 'Consent ID', 'consentfold' ), 'value' => $record['consent_id'], ), array( 'name' => __( 'Date (UTC)', 'consentfold' ), 'value' => $record['recorded_at'], ), array( 'name' => __( 'Categories', 'consentfold' ), 'value' => implode( ', ', $record['categories'] ), ), array( 'name' => __( 'Method', 'consentfold' ), 'value' => $record['method'], ), array( 'name' => __( 'Region', 'consentfold' ), 'value' => (string) $record['region'], ), array( 'name' => __( 'Language', 'consentfold' ), 'value' => (string) $record['lang'], ), ), ); } } return array( 'data' => $data, 'done' => $done, ); } }