Export starts writing the WXR download: the * whole site's content leaving it, which is worth a row whoever does it. * - `wp_privacy_personal_data_export_file_created` fires once the personal * data export archive exists; `wp_privacy_personal_data_erased` once every * eraser -- this plugin's own among them -- has finished. * * A privacy row names the REQUEST (object `user_request`, its post id), never * the person: their email address is what the request is about, and a log row * holding it would be a copy of exactly the personal data the erasure just * removed -- one this plugin's own eraser could not find again, since it * matches rows by actor and by `user` object. The archive's path and its * download URL, which core also passes, are never stored: the URL is a * download link to someone's personal data. * * The same reasoning is why EventLogger no longer writes post events for the * `user_request` post type at all (INTERNAL_POST_TYPES): core stores the * requester's email address as that post's TITLE, and "User_request * "alice@example.com" created" was a row no erasure could reach. * * Importing is a named deferral: core ships no importer, and the hooks * (`import_start`/`import_end`) belong to the WordPress Importer plugin. */ final class ToolsEvents { public const SITE_EXPORTED = 'site_exported'; public const PERSONAL_EXPORTED = 'personal_data_exported'; public const PERSONAL_ERASED = 'personal_data_erased'; /** * @return array Event type => label for the log's filter. */ public static function event_types(): array { return array( self::SITE_EXPORTED => 'Content Exported', self::PERSONAL_EXPORTED => 'Personal Data Exported', self::PERSONAL_ERASED => 'Personal Data Erased', ); } public static function register(): void { add_action( 'export_wp', array( __CLASS__, 'log_site_export' ) ); // Registered for FOUR arguments so the request id arrives; the archive // path, URL and report path are received and deliberately dropped. add_action( 'wp_privacy_personal_data_export_file_created', array( __CLASS__, 'log_personal_data_export' ), 10, 4 ); add_action( 'wp_privacy_personal_data_erased', array( __CLASS__, 'log_personal_data_erasure' ) ); } /** * @param mixed $args export_wp()'s arguments; only `content` is read. */ public static function log_site_export( $args ): void { $content = is_array( $args ) && isset( $args['content'] ) && is_string( $args['content'] ) ? sanitize_key( $args['content'] ) : 'all'; $content = '' !== $content ? EventLogger::bounded( $content, 40 ) : 'all'; EventLogger::log( self::SITE_EXPORTED, 'all' === $content ? 'Site content exported (all content)' : sprintf( 'Site content exported (%s)', $content ), 'export', null, array( 'content' => $content ) ); } /** * @param mixed $archive_pathname Not stored. * @param mixed $archive_url Not stored. * @param mixed $report_pathname Not stored. * @param mixed $request_id The request. */ public static function log_personal_data_export( $archive_pathname, $archive_url, $report_pathname, $request_id ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed -- see register(). self::log_request( self::PERSONAL_EXPORTED, 'Personal data export file created for request #%d', (int) $request_id ); } /** * @param mixed $request_id The request. */ public static function log_personal_data_erasure( $request_id ): void { self::log_request( self::PERSONAL_ERASED, 'Personal data erased for request #%d', (int) $request_id ); } /** * @param string $type Event type. * @param string $summary Summary format; the request id fills it. * @param int $request_id The request. */ private static function log_request( string $type, string $summary, int $request_id ): void { if ( $request_id <= 0 ) { return; } EventLogger::log( $type, sprintf( $summary, $request_id ), 'user_request', $request_id ); } }