__('Aiwend AI Forms', 'aiwend'), 'callback' => static fn(string $email, int $page = 1): array => self::export($plugin, $email, $page)]; return $exporters; }); add_filter('wp_privacy_personal_data_erasers', static function (array $erasers) use ($plugin): array { $erasers['aiwend-forms'] = ['eraser_friendly_name' => __('Aiwend AI Forms', 'aiwend'), 'callback' => static fn(string $email, int $page = 1): array => self::erase($plugin, $email, $page)]; return $erasers; }); } /** Daily sweep for the current site. Returns the number of rows removed. */ public static function sweep(Plugin $plugin, int $now, int $batch = self::SWEEP_BATCH, int $batches = self::SWEEP_BATCHES): int { $store = AiFormsConfig::store($plugin); $site = get_current_blog_id(); $removed = 0; for ($i = 0; $i < $batches; $i++) { $n = $store->expire($site, $now, $batch); $removed += $n; if ($n < $batch) { break; } } return $removed; } /** * WordPress calls this only for a personal-data request whose email address the requester has confirmed (the core request flow), and * only for the CURRENT site, so a record of another site or of an unverified address is never returned. * * @return array{data: list>, done: bool} */ public static function export(Plugin $plugin, string $email, int $page = 1): array { $site = get_current_blog_id(); $store = AiFormsConfig::store($plugin); $rows = $store->byEmail($site, $email, (max(1, $page) - 1) * self::EXPORT_PAGE, self::EXPORT_PAGE); $data = []; foreach ($rows as $r) { $items = [ ['name' => __('Form', 'aiwend'), 'value' => (string) (AiFormsConfig::forms()[(int) $r['form_id']]['title'] ?? ('#' . (int) $r['form_id']))], ['name' => __('Submitted', 'aiwend'), 'value' => gmdate('Y-m-d H:i:s', (int) $r['created_at']) . ' UTC'], ['name' => __('Name', 'aiwend'), 'value' => (string) $r['name']], ['name' => __('Email', 'aiwend'), 'value' => (string) $r['email']], ['name' => __('Topic', 'aiwend'), 'value' => (string) $r['topic']], ['name' => __('Message', 'aiwend'), 'value' => (string) $r['message']], ['name' => __('Expires', 'aiwend'), 'value' => gmdate('Y-m-d H:i:s', (int) $r['expires_at']) . ' UTC'], ]; if ((string) $r['analysis_state'] === 'done') { $items[] = ['name' => __('AI summary (administrator analysis)', 'aiwend'), 'value' => (string) $r['summary']]; $items[] = ['name' => __('AI category', 'aiwend'), 'value' => (string) $r['category']]; $items[] = ['name' => __('AI reply draft', 'aiwend'), 'value' => (string) $r['reply']]; } $data[] = ['group_id' => 'aiwend-forms', 'group_label' => __('Contact form submissions', 'aiwend'), 'item_id' => 'aiwend-form-' . $site . '-' . (int) $r['id'], 'data' => $items]; } return ['data' => $data, 'done' => count($rows) < self::EXPORT_PAGE]; } /** @return array{items_removed: bool, items_retained: bool, messages: list, done: bool} */ public static function erase(Plugin $plugin, string $email, int $page = 1): array { $site = get_current_blog_id(); $store = AiFormsConfig::store($plugin); $removed = $store->eraseEmail($site, $email, 200); $more = $store->countForEmail($site, $email) > 0; return [ 'items_removed' => $removed > 0, 'items_retained' => false, 'messages' => $removed > 0 ? [__('Contact form submissions on this site, including any AI summary or reply draft stored with them, were deleted. If an administrator had the text analysed, the AI provider may still hold its own copy under its own terms; deleting it here does not delete that copy, and backups of this site are not rewritten.', 'aiwend')] : [], 'done' => !$more, ]; } }