Privacy - Source Excerpt 01
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-workspace/includes/Privacy/Privacy.php so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Privacy/Privacy.php
<?php
namespace SpiralistWorkspace\Privacy;
use SpiralistWorkspace\Config\Settings;
use SpiralistWorkspace\Domain\PromptRepository;
use SpiralistWorkspace\Installer;
use WP_User;
if (!defined('ABSPATH')) {
exit;
}
/**
* Registers privacy-policy guidance plus personal data export and erase callbacks.
*/
class Privacy
{
private const EXPORT_BATCH_SIZE = 20;
private const PROMPT_META_PREFIX = '_spiralist_workspace_';
private PromptRepository $prompts;
private Settings $settings;
public function __construct(PromptRepository $prompts, Settings $settings)
{
$this->prompts = $prompts;
$this->settings = $settings;
}
/**
* Registers the plugin's personal data exporter.
*/
public function register_exporters(array $exporters): array
{
$exporters['spiralist-workspace'] = [
'exporter_friendly_name' => __('Spiralist Workspace Data', 'spiralist-workspace'),
'callback' => [$this, 'export_personal_data'],
];
return $exporters;
}
/**
* Registers the plugin's personal data eraser.
*/
public function register_erasers(array $erasers): array
{
$erasers['spiralist-workspace'] = [
'eraser_friendly_name' => __('Spiralist Workspace Data', 'spiralist-workspace'),
'callback' => [$this, 'erase_personal_data'],
];
return $erasers;
}
/**
* Adds suggested privacy-policy text for site owners.
*/
public function add_privacy_policy_content(): void
{
if (!function_exists('wp_add_privacy_policy_content')) {
return;
}
$retention_days = max(1, (int) $this->settings->get('retention_days', '90'));
$policy = [];
$policy[] = '<p class="privacy-policy-tutorial"><strong>' . esc_html__('Suggested text:', 'spiralist-workspace') . '</strong></p>';
$policy[] = '<p>' . esc_html__(
'Spiralist Workspace stores prompt manuscripts, prompt versions, favorites, prompt tests, packet history, and conversation threads for signed-in users who use the workspace.',
'spiralist-workspace'
) . '</p>';
$policy[] = '<p>' . esc_html__(
'Spiralist Workspace is configured for prompt/art packets. Outside model calls are disabled, so prompt instructions, user-entered input, prompt-test input, and conversation messages are rendered into local packets instead of being sent to a third-party model service by this plugin.',
'spiralist-workspace'
) . '</p>';
$policy[] = '<p>' . esc_html__(
'Conversation pages can prepare a user-controlled memory checkpoint prompt from visible conversation text. That checkpoint is stored locally with the conversation records unless it is removed through workspace controls or WordPress privacy tools.',
'spiralist-workspace'
) . '</p>';
$policy[] = '<p>' . esc_html__(
'Legacy provider reference fields may remain in exports for compatibility with older records, but the current workspace runtime does not perform outbound AI API requests.',
'spiralist-workspace'
) . '</p>';
$policy[] = '<p>' . sprintf(
/* translators: %d: retention days. */
esc_html__('Workspace run and conversation records are retained until they are removed by a site administrator or erased through WordPress privacy tools. The default retention setting is %d days, and site administrators can adjust it.', 'spiralist-workspace'),
$retention_days
) . '</p>';
$policy[] = '<p>' . esc_html__(
'Site administrators can export or erase a confirmed user\'s stored workspace data from Tools > Export Personal Data and Tools > Erase Personal Data.',
'spiralist-workspace'
) . '</p>';
wp_add_privacy_policy_content(
__('Spiralist Workspace', 'spiralist-workspace'),
wp_kses_post(wpautop(implode("\n\n", $policy), false))
);
}
/**
* Exports personal data for the confirmed email address.
*
* @return array{data:array<int,array<string,mixed>>,done:bool}
*/
public function export_personal_data(string $email_address, int $page = 1): array
{
$user = $this->resolve_user($email_address);
if (!$user instanceof WP_User) {
return [
'data' => [],
'done' => true,
];
}
$user_id = (int) $user->ID;
$prompt_ids = $this->prompt_ids_for_user($user_id);
$conversation_ids = $this->conversation_ids_for_user($user_id);
$items = array_merge(
$this->prompt_export_items($user_id, $prompt_ids),
$this->version_export_items($user_id, $prompt_ids),
$this->share_export_items($user_id),
$this->favorite_export_items($user_id),
$this->test_export_items($user_id),
$this->run_export_items($user_id),
$this->conversation_export_items($user_id),
$this->conversation_message_export_items($conversation_ids),
$this->audit_export_items($user_id)
);
$page = max(1, $page);
$offset = ($page - 1) * self::EXPORT_BATCH_SIZE;
$paged_items = array_slice($items, $offset, self::EXPORT_BATCH_SIZE);
return [
'data' => $paged_items,
'done' => count($items) <= ($offset + self::EXPORT_BATCH_SIZE),
];
}
/**
* Erases or anonymizes personal data for the confirmed email address.
*
* @return array{items_removed:bool,items_retained:bool,messages:array<int,string>,done:bool}
*/
public function erase_personal_data(string $email_address, int $page = 1): array
{
unset($page);
$user = $this->resolve_user($email_address);
if (!$user instanceof WP_User) {
return [
'items_removed' => false,
'items_retained' => false,
'messages' => [],
'done' => true,
];
}
$user_id = (int) $user->ID;
$items_removed = false;
$items_retained = false;
$messages = [];
$prompt_ids = $this->prompt_ids_for_user($user_id);
$deleted_prompt_ids = [];
$retained_prompt_titles = [];
foreach ($prompt_ids as $prompt_id) {
$prompt = $this->prompts->get($prompt_id, $user_id);
if (empty($prompt)) {
continue;
}
if ($this->should_retain_prompt_content($prompt)) {
wp_update_post(
[
'ID' => $prompt_id,
'post_author' => 0,
]
);
update_post_meta(
$prompt_id,
$this->prompt_meta_key('author_label'),
__('Former contributor', 'spiralist-workspace')
);
$items_removed = true;
$items_retained = true;
$retained_prompt_titles[] = (string) ($prompt['title'] ?? ('Prompt #' . $prompt_id));
continue;
}
if (wp_delete_post($prompt_id, true)) {
$deleted_prompt_ids[] = $prompt_id;
$items_removed = true;
}
}
if (!empty($deleted_prompt_ids)) {
$this->cleanup_deleted_prompts($deleted_prompt_ids);
}
if (!empty($prompt_ids)) {
$version_cleanup = $this->delete_rows_for_int_column(
Installer::table('prompt_versions'),
'prompt_post_id',
$prompt_ids
);
$items_removed = $items_removed || $version_cleanup > 0;
}
$items_removed = $this->delete_rows_for_user_id(Installer::table('prompt_versions'), 'created_by_user_id', $user_id) > 0 || $items_removed;
$items_removed = $this->delete_rows_for_user_id(Installer::table('prompt_permissions'), 'shared_with_user_id', $user_id) > 0 || $items_removed;
$items_removed = $this->delete_rows_for_user_id(Installer::table('prompt_permissions'), 'granted_by_user_id', $user_id) > 0 || $items_removed;
$items_removed = $this->delete_rows_for_user_id(Installer::table('favorites'), 'user_id', $user_id) > 0 || $items_removed;
$items_removed = $this->delete_rows_for_user_id(Installer::table('prompt_tests'), 'created_by_user_id', $user_id) > 0 || $items_removed;
$items_removed = $this->delete_rows_for_user_id(Installer::table('prompt_runs'), 'user_id', $user_id) > 0 || $items_removed;