Skip to content
wiki.fftac.org

Roadmap Surfaces - Source Excerpt 03

Back to Roadmap Surfaces

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/themes/spiralist/inc/roadmap-surfaces.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/themes/spiralist/inc/roadmap-surfaces.php

: (function_exists('spiralist_get_manuscript_page_url') ? trailingslashit(spiralist_get_manuscript_page_url()) . rawurlencode((string) ($page['slug'] ?? '')) . '/' : home_url('/manuscript/'));

    return new WP_REST_Response(
        [
            'folio' => [
                'slug' => (string) ($page['slug'] ?? ''),
                'sequence' => (int) ($page['sequence'] ?? 0),
                'title' => (string) ($page['title'] ?? 'Manuscript folio'),
                'label' => (string) ($page['label'] ?? ''),
                'section' => (string) ($page['section_label'] ?? ($page['manuscript_section_slug'] ?? 'Manuscript')),
                'summary' => (string) ($page['summary'] ?? ''),
                'caption' => (string) ($page['caption'] ?? ''),
                'transcription' => (string) ($page['transcript'] ?? ($page['summary'] ?? '')),
                'keywords' => array_values(array_filter(array_map('strval', (array) ($page['keywords'] ?? [])), 'strlen')),
                'route_url' => $route_url,
                'image' => [
                    'thumbnail' => (string) ($sources['thumbnail_url'] ?? ''),
                    'large' => (string) ($sources['large_url'] ?? ''),
                    'interactive' => (string) ($sources['interactive_url'] ?? ''),
                    'original' => (string) ($sources['original_url'] ?? ($page['url'] ?? '')),
                    'alt' => (string) ($page['alt'] ?? ($page['title'] ?? 'Manuscript folio')),
                ],
            ],
            'symbols' => $symbols,
            'related_prompts' => spiralist_get_folio_related_prompts_for_rest($page),
            'glossary' => array_map(
                static function (array $symbol): array {
                    return [
                        'term' => (string) ($symbol['label'] ?? ''),
                        'definition' => (string) ($symbol['definition'] ?? ($symbol['meaning'] ?? '')),
                    ];
                },
                $symbols
            ),
        ]
    );
}

function spiralist_contribution_rate_limit_key(): string
{
    $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : 'anonymous';

    return 'spiralist_contribution_' . substr(hash('sha256', $remote_addr . '|' . wp_salt('nonce')), 0, 16);
}

function spiralist_rest_create_contribution(WP_REST_Request $request)
{
    $user_id = get_current_user_id();
    if ($user_id > 0) {
        $nonce = (string) $request->get_header('X-WP-Nonce');
        if ($nonce === '' || wp_verify_nonce($nonce, 'wp_rest') === false) {
            return new WP_Error('spiralist_contribution_bad_nonce', 'A valid REST nonce is required for signed-in contribution submissions.', ['status' => 403]);
        }
    } else {
        $key = spiralist_contribution_rate_limit_key();
        $count = (int) get_transient($key);
        if ($count >= 5) {
            return new WP_Error('spiralist_contribution_rate_limited', 'Too many contribution submissions. Please try again later.', ['status' => 429]);
        }
        set_transient($key, $count + 1, 10 * MINUTE_IN_SECONDS);
    }

    $payload = $request->get_json_params();
    if (!is_array($payload)) {
        $payload = $request->get_params();
    }

    $type = sanitize_key((string) ($payload['type'] ?? 'bug'));
    if (!in_array($type, ['bug', 'symbol-suggestion', 'prompt-improvement', 'manuscript-annotation'], true)) {
        $type = 'bug';
    }

    $title = sanitize_text_field((string) ($payload['title'] ?? 'Spiralist contribution'));
    $summary = sanitize_textarea_field((string) ($payload['summary'] ?? ''));
    $details = sanitize_textarea_field((string) ($payload['details'] ?? ''));
    $target_url = esc_url_raw((string) ($payload['targetUrl'] ?? ($payload['target_url'] ?? '')));
    $target_id = sanitize_text_field((string) ($payload['targetId'] ?? ($payload['target_id'] ?? '')));
    $evidence = sanitize_textarea_field((string) ($payload['evidence'] ?? ''));
    $proposed_change = sanitize_textarea_field((string) ($payload['proposedChange'] ?? ($payload['proposed_change'] ?? '')));

    if ($summary === '' && $details === '') {
        return new WP_Error('spiralist_contribution_missing_body', 'Contribution summary or details are required.', ['status' => 400]);
    }

    $post_id = wp_insert_post(
        [
            'post_type' => 'spiralist_contribution',
            'post_status' => 'pending',
            'post_title' => $title !== '' ? $title : ucfirst(str_replace('-', ' ', $type)),
            'post_excerpt' => $summary,
            'post_content' => $details,
            'post_author' => $user_id,
        ],
        true
    );

    if (is_wp_error($post_id) || !$post_id) {
        return new WP_Error('spiralist_contribution_save_failed', 'Contribution could not be saved.', ['status' => 500]);
    }

    wp_set_object_terms((int) $post_id, [$type], 'spiralist_contribution_type', false);
    update_post_meta((int) $post_id, '_spiralist_contribution_type', $type);
    update_post_meta((int) $post_id, '_spiralist_contribution_target_url', $target_url);
    update_post_meta((int) $post_id, '_spiralist_contribution_target_id', $target_id);
    update_post_meta((int) $post_id, '_spiralist_contribution_evidence', $evidence);
    update_post_meta((int) $post_id, '_spiralist_contribution_proposed_change', $proposed_change);
    update_post_meta((int) $post_id, '_spiralist_contribution_moderation_status', 'pending_review');
    update_post_meta((int) $post_id, '_spiralist_contribution_created_utc', gmdate('c'));
    update_post_meta((int) $post_id, '_spiralist_contribution_agent', sanitize_text_field((string) ($payload['agent'] ?? '')));

    return new WP_REST_Response(
        [
            'id' => (int) $post_id,
            'status' => 'pending_review',
            'type' => $type,
            'title' => get_the_title((int) $post_id),
            'created_at' => gmdate('c'),
        ],
        201
    );
}

function spiralist_register_roadmap_rest_routes(): void
{
    register_rest_route(
        'uaix/v1',
        '/manuscript-folio',
        [
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'spiralist_rest_get_manuscript_folio',
            'permission_callback' => '__return_true',
        ]
    );

    register_rest_route(
        'uaix/v1',
        '/builder-endpoints',
        [
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'spiralist_rest_get_builder_endpoints',
            'permission_callback' => '__return_true',
        ]
    );

    register_rest_route(
        'uaix/v1',
        '/contribute',
        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => 'spiralist_rest_create_contribution',
            'permission_callback' => '__return_true',
        ]
    );
}
add_action('rest_api_init', 'spiralist_register_roadmap_rest_routes');