Skip to content
wiki.fftac.org

Privacy - Source Excerpt 04

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

global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('conversations') . ' WHERE owner_user_id = %d ORDER BY created_utc ASC, id ASC',
            $user_id
        );

        $rows = $wpdb->get_results($sql, ARRAY_A);
        if (!is_array($rows)) {
            return [];
        }

        $items = [];
        foreach ($rows as $row) {
            $data = [];
            $this->add_export_pair($data, __('Conversation ID', 'spiralist-workspace'), $row['id'] ?? 0);
            $this->add_export_pair($data, __('Prompt ID', 'spiralist-workspace'), $row['prompt_post_id'] ?? 0);
            $this->add_export_pair($data, __('Prompt title', 'spiralist-workspace'), $this->prompt_title((int) ($row['prompt_post_id'] ?? 0)));
            $this->add_export_pair($data, __('Title', 'spiralist-workspace'), $row['title'] ?? '');
            $this->add_export_pair($data, __('Visibility', 'spiralist-workspace'), $row['visibility'] ?? '');
            $this->add_export_pair($data, __('Conversation settings JSON', 'spiralist-workspace'), $row['conversation_settings_json'] ?? '');
            $this->add_export_pair($data, __('Runtime reference', 'spiralist-workspace'), $row['openai_reference'] ?? '');
            $this->add_export_pair($data, __('Created UTC', 'spiralist-workspace'), $row['created_utc'] ?? '');
            $this->add_export_pair($data, __('Updated UTC', 'spiralist-workspace'), $row['updated_utc'] ?? '');
            $this->add_export_pair($data, __('Archived UTC', 'spiralist-workspace'), $row['archived_utc'] ?? '');

            $items[] = $this->build_export_item(
                'spiralist_workspace_conversations',
                __('Workspace Conversations', 'spiralist-workspace'),
                'workspace-conversation-' . (int) ($row['id'] ?? 0),
                $data
            );
        }

        return $items;
    }

    /**
     * Returns conversation-message export items.
     *
     * @param int[] $conversation_ids
     * @return array<int,array<string,mixed>>
     */
    private function conversation_message_export_items(array $conversation_ids): array
    {
        global $wpdb;

        if (empty($conversation_ids)) {
            return [];
        }

        $sql = 'SELECT * FROM ' . Installer::table('conversation_messages') . ' WHERE conversation_id IN (' . implode(',', array_map('intval', $conversation_ids)) . ') ORDER BY created_utc ASC, id ASC';
        $rows = $wpdb->get_results($sql, ARRAY_A);
        if (!is_array($rows)) {
            return [];
        }

        $items = [];
        foreach ($rows as $row) {
            $data = [];
            $this->add_export_pair($data, __('Message ID', 'spiralist-workspace'), $row['id'] ?? 0);
            $this->add_export_pair($data, __('Conversation ID', 'spiralist-workspace'), $row['conversation_id'] ?? 0);
            $this->add_export_pair($data, __('Role', 'spiralist-workspace'), $row['role'] ?? '');
            $this->add_export_pair($data, __('Content', 'spiralist-workspace'), $row['content'] ?? '');
            $this->add_export_pair($data, __('Runtime reference', 'spiralist-workspace'), $row['openai_reference'] ?? '');
            $this->add_export_pair($data, __('Metadata JSON', 'spiralist-workspace'), $row['metadata_json'] ?? '');
            $this->add_export_pair($data, __('Created UTC', 'spiralist-workspace'), $row['created_utc'] ?? '');

            $items[] = $this->build_export_item(
                'spiralist_workspace_conversation_messages',
                __('Workspace Conversation Messages', 'spiralist-workspace'),
                'workspace-conversation-message-' . (int) ($row['id'] ?? 0),
                $data
            );
        }

        return $items;
    }

    /**
     * Returns audit-log export items.
     *
     * @return array<int,array<string,mixed>>
     */
    private function audit_export_items(int $user_id): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('audit_log') . ' WHERE actor_user_id = %d ORDER BY created_utc ASC, id ASC',
            $user_id
        );

        $rows = $wpdb->get_results($sql, ARRAY_A);
        if (!is_array($rows)) {
            return [];
        }

        $items = [];
        foreach ($rows as $row) {
            $data = [];
            $this->add_export_pair($data, __('Audit ID', 'spiralist-workspace'), $row['id'] ?? 0);
            $this->add_export_pair($data, __('Event type', 'spiralist-workspace'), $row['event_type'] ?? '');
            $this->add_export_pair($data, __('Object type', 'spiralist-workspace'), $row['object_type'] ?? '');
            $this->add_export_pair($data, __('Object ID', 'spiralist-workspace'), $row['object_id'] ?? '');
            $this->add_export_pair($data, __('Context JSON', 'spiralist-workspace'), $row['context_json'] ?? '');
            $this->add_export_pair($data, __('Created UTC', 'spiralist-workspace'), $row['created_utc'] ?? '');

            $items[] = $this->build_export_item(
                'spiralist_workspace_audit_log',
                __('Workspace Audit Events', 'spiralist-workspace'),
                'workspace-audit-' . (int) ($row['id'] ?? 0),
                $data
            );
        }

        return $items;
    }

    /**
     * Returns prompt post IDs for a user.
     *
     * @return int[]
     */
    private function prompt_ids_for_user(int $user_id): array
    {
        $ids = get_posts(
            [
                'post_type' => PromptRepository::POST_TYPE,
                'post_status' => 'any',
                'author' => $user_id,
                'fields' => 'ids',
                'numberposts' => -1,
                'orderby' => 'ID',
                'order' => 'ASC',
            ]
        );

        return array_values(array_map('intval', is_array($ids) ? $ids : []));
    }

    /**
     * Returns conversation IDs for a user.
     *
     * @return int[]
     */
    private function conversation_ids_for_user(int $user_id): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT id FROM ' . Installer::table('conversations') . ' WHERE owner_user_id = %d ORDER BY id ASC',
            $user_id
        );

        $ids = $wpdb->get_col($sql);

        return array_values(array_map('intval', is_array($ids) ? $ids : []));
    }

    /**
     * Deletes related custom-table data for prompts that were permanently removed.
     *
     * @param int[] $prompt_ids
     */
    private function cleanup_deleted_prompts(array $prompt_ids): void
    {
        global $wpdb;

        $prompt_ids = array_values(array_unique(array_filter(array_map('intval', $prompt_ids))));
        if (empty($prompt_ids)) {
            return;
        }

        $prompt_list = implode(',', $prompt_ids);
        $conversation_ids = $wpdb->get_col(
            'SELECT id FROM ' . Installer::table('conversations') . ' WHERE prompt_post_id IN (' . $prompt_list . ')'
        );
        $conversation_ids = array_values(array_map('intval', is_array($conversation_ids) ? $conversation_ids : []));

        $this->delete_rows_for_int_column(Installer::table('prompt_versions'), 'prompt_post_id', $prompt_ids);
        $this->delete_rows_for_int_column(Installer::table('prompt_permissions'), 'prompt_post_id', $prompt_ids);
        $this->delete_rows_for_int_column(Installer::table('favorites'), 'prompt_post_id', $prompt_ids);
        $this->delete_rows_for_int_column(Installer::table('prompt_tests'), 'prompt_post_id', $prompt_ids);
        $this->delete_rows_for_int_column(Installer::table('prompt_runs'), 'prompt_post_id', $prompt_ids);

        if (!empty($conversation_ids)) {
            $this->delete_rows_for_int_column(Installer::table('conversation_messages'), 'conversation_id', $conversation_ids);
            $this->delete_rows_for_int_column(Installer::table('prompt_runs'), 'conversation_id', $conversation_ids);
            $this->delete_rows_for_int_column(Installer::table('conversations'), 'id', $conversation_ids);
        }
    }

    /**
     * Deletes rows from a table by user ID.
     */
    private function delete_rows_for_user_id(string $table, string $column, int $user_id): int
    {
        global $wpdb;

        return (int) $wpdb->delete(
            $table,
            [
                $column => $user_id,
            ],
            ['%d']
        );
    }

    /**
     * Deletes rows from a table where an integer column matches any supplied IDs.
     *
     * @param int[] $ids
     */
    private function delete_rows_for_int_column(string $table, string $column, array $ids): int
    {
        global $wpdb;

        $ids = array_values(array_unique(array_filter(array_map('intval', $ids))));