prefix . 'avai_objects', '/' ); $checking = false; $actor = get_current_user_id(); $site = get_current_blog_id(); $verify = static function () use ( $guard, $actor, $site, &$checking ): void { if ( $checking ) { throw new \RuntimeException( 'A nested investigation mutation interrupted this action.' ); } $checking = true; try { $guard(); if ( $actor !== get_current_user_id() || $site !== get_current_blog_id() ) { throw new \RuntimeException( 'The administrator or site changed at the investigation write boundary.' ); } } finally { $checking = false; } }; $query = static function ( $sql ) use ( $table, $id, $verify ) { if ( is_string( $sql ) && str_contains( $sql, $id ) && preg_match( '/^\s*(?:UPDATE\s+|INSERT\s+INTO\s+|DELETE\s+FROM\s+)`?' . $table . '`?\s/i', $sql ) ) { $verify(); } return $sql; }; add_filter( 'query', $query, PHP_INT_MAX ); try { $verify(); $result = $operation(); $verify(); return $result; } finally { remove_filter( 'query', $query, PHP_INT_MAX ); } } public static function register(): void { add_filter( 'wp_privacy_personal_data_exporters', static function ( array $exporters ): array { $exporters['shieldlume-review-notes'] = array( 'exporter_friendly_name' => __( 'Shieldlume investigation notes', 'shieldlume-security' ), 'callback' => static function ( string $email, int $page = 1 ): array|\WP_Error { try { return self::export( $email, $page ); } catch ( \Throwable $error ) { return new \WP_Error( 'shieldlume_review_notes_export_unavailable', __( 'Investigation notes could not be exported safely. Review administrator access and retained records, then retry this privacy request.', 'shieldlume-security' ) ); } } ); return $exporters; } ); add_filter( 'wp_privacy_personal_data_erasers', static function ( array $erasers ): array { $erasers['shieldlume-review-notes'] = array( 'eraser_friendly_name' => __( 'Shieldlume investigation notes', 'shieldlume-security' ), 'callback' => array( self::class, 'erase' ) ); return $erasers; } ); } private static function context( string $email, int $page ): array { $actor = get_current_user_id(); $site = get_current_blog_id(); self::authority( $actor, $site ); if ( $page < 1 || $page > 20 || '' === $email || sanitize_email( $email ) !== $email ) { throw new \RuntimeException( 'Current single-site administrator access and a valid privacy request are required.' ); } $user = get_user_by( 'email', $email ); self::authority( $actor, $site ); return array( 'actor' => $actor, 'site' => $site, 'subject' => $user ? (int) $user->ID : 0, 'email' => $email ); } private static function authority( int $actor, int $site ): void { if ( is_multisite() || $actor < 1 || $actor !== get_current_user_id() || $site !== get_current_blog_id() ) { throw new \RuntimeException( 'Current single-site administrator access is required.' ); } if ( ! WorkflowAuthority::administrator() || $actor !== get_current_user_id() || $site !== get_current_blog_id() ) { throw new \RuntimeException( 'Administrator or site access changed. Retry the privacy request.' ); } } private static function assertContext( array $context ): void { self::authority( $context['actor'], $context['site'] ); if ( $context['subject'] ) { clean_user_cache( $context['subject'] ); $user = get_userdata( $context['subject'] ); if ( ! $user || 0 !== strcasecmp( $user->user_email, $context['email'] ) ) { throw new \RuntimeException( 'The account associated with this privacy request changed. Review it before retrying.' ); } } self::authority( $context['actor'], $context['site'] ); } private static function own( array $row, int $subject ): bool { if ( 'investigation_review' !== ( $row['_kind'] ?? '' ) || $subject < 1 || $subject !== (int) ( $row['actor'] ?? 0 ) || ! array_key_exists( 'annotations', $row ) ) { return false; } if ( ! is_array( $row['annotations'] ) ) { throw new \RuntimeException( 'Retained investigation notes need a separate review before this privacy request can finish.' ); } return true; } public static function export( string $email, int $page = 1 ): array { $context = self::context( $email, $page ); $store = new Store(); $data = array(); $selected = array(); if ( ! $context['subject'] ) { return array( 'data' => array(), 'done' => true ); } $lock = $store->lock( 'pro-review-write', 120 ); if ( ! $lock ) { throw new \RuntimeException( 'Investigation notes are being changed. Retry this privacy export shortly.' ); } try { if ( $store->count( 'investigation_review' ) > 2000 ) { throw new \RuntimeException( 'The investigation privacy record limit was exceeded. Review retained records before retrying.' ); } $rows = $store->list( 'investigation_review', null, self::PAGE_SIZE, ( $page - 1 ) * self::PAGE_SIZE ); foreach ( $rows as $row ) { if ( ! self::own( $row, $context['subject'] ) ) { continue; } $fields = array(); foreach ( array( 'notes' => __( 'My investigation notes', 'shieldlume-security' ), 'next_action' => __( 'My next action', 'shieldlume-security' ), 'next_review_date' => __( 'Next-review date', 'shieldlume-security' ) ) as $key => $label ) { $value = $row['annotations'][ $key ] ?? ''; if ( is_string( $value ) && '' !== $value ) { $fields[] = array( 'name' => $label, 'value' => $value ); } } if ( $fields ) { $selected[] = $row; $data[] = array( 'group_id' => 'shieldlume-review-notes', 'group_label' => __( 'Shieldlume investigation notes', 'shieldlume-security' ), 'item_id' => 'shieldlume-notes-' . hash( 'sha256', $row['_id'] ), 'data' => $fields ); } } self::assertContext( $context ); if ( ! $store->ownsLock( 'pro-review-write', $lock ) ) { throw new \RuntimeException( 'The investigation export lock expired. Retry the privacy request.' ); } foreach ( $selected as $row ) { if ( $store->get( $row['_id'] ) !== $row ) { throw new \RuntimeException( 'Investigation notes changed or were erased during export. Retry the current privacy request.' ); } } if ( $context['actor'] !== get_current_user_id() || $context['site'] !== get_current_blog_id() ) { throw new \RuntimeException( 'The administrator or site changed during privacy export.' ); } return array( 'data' => $data, 'done' => count( $rows ) < self::PAGE_SIZE || $page >= 20 ); } finally { $store->unlock( 'pro-review-write', $lock ); } } public static function erase( string $email, int $page = 1 ): array { $removed = false; $store = null; $lock = null; try { $context = self::context( $email, $page ); if ( ! $context['subject'] ) { return array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true ); } $store = new Store(); $lock = $store->lock( 'pro-review-write', 120 ); if ( ! $lock ) { throw new \RuntimeException( 'Another request is updating investigation notes. Retry erasure shortly.' ); } if ( $store->count( 'investigation_review' ) > 2000 ) { throw new \RuntimeException( 'The investigation privacy record limit was exceeded. Review retained records before retrying.' ); } $rows = $store->list( 'investigation_review', null, self::PAGE_SIZE, ( $page - 1 ) * self::PAGE_SIZE ); foreach ( $rows as $row ) { if ( ! self::own( $row, $context['subject'] ) ) { continue; } self::assertContext( $context ); $current = $store->get( $row['_id'] ); if ( ! $current || $current !== $row || ! $store->ownsLock( 'pro-review-write', $lock ) ) { throw new \RuntimeException( 'An investigation note or its lock changed. Retry erasure.' ); } unset( $current['annotations'] ); $guard = static function () use ( $context, $store, $lock ): void { self::assertContext( $context ); if ( ! $store->ownsLock( 'pro-review-write', $lock ) ) { throw new \RuntimeException( 'The investigation privacy lock expired. Retry erasure.' ); } }; self::mutate( $row['_id'], $guard, static fn() => $store->put( 'investigation_review', $row['_id'], $current, $row['_status'], (int) $row['_revision'] ) ); $removed = true; } self::assertContext( $context ); return array( 'items_removed' => $removed, 'items_retained' => true, 'messages' => array( __( 'Matching author notes, next actions and review dates are removed. Original findings, security evidence and evidence-review progress remain for a separate authorized retention review.', 'shieldlume-security' ) ), 'done' => count( $rows ) < self::PAGE_SIZE || $page >= 20 ); } catch ( \Throwable $error ) { return array( 'items_removed' => $removed, 'items_retained' => true, 'messages' => array( __( 'Investigation notes could not be fully erased. Restore access and retry this privacy request; no successful completion is inferred.', 'shieldlume-security' ) ), 'done' => true ); } finally { if ( $store && $lock ) { $store->unlock( 'pro-review-write', $lock ); } } } }