Skip to content
wiki.fftac.org

Privacy - Source Excerpt 01

Back to Privacy

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;