settings = $settings; } /** * Register routes. */ public function register() { add_action( 'rest_api_init', array( $this, 'routes' ) ); } /** * Define the routes. */ public function routes() { register_rest_route( self::NAMESPACE, '/settings', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'can_manage' ), ), array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'save_settings' ), 'permission_callback' => array( $this, 'can_manage' ), ), ) ); register_rest_route( self::NAMESPACE, '/settings/reset', array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'reset_settings' ), 'permission_callback' => array( $this, 'can_manage' ), ) ); register_rest_route( self::NAMESPACE, '/onboarding', array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'complete_onboarding' ), 'permission_callback' => array( $this, 'can_manage' ), ) ); register_rest_route( self::NAMESPACE, '/readiness', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_readiness' ), 'permission_callback' => array( $this, 'can_manage' ), ) ); // JSON-LD preview: the exact @graph the front end would emit for the site // or a chosen post, so an owner can inspect and validate it without viewing // page source. `post` = 0/absent → the site-wide identity graph. register_rest_route( self::NAMESPACE, '/preview/schema', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_schema_preview' ), 'permission_callback' => array( $this, 'can_manage' ), 'args' => array( 'post' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), ), ) ); // The Markdown twin of a page/post — what an agent receives when it requests // the page as text/markdown. Per-page, so the site target has no Markdown. register_rest_route( self::NAMESPACE, '/preview/markdown', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_markdown_preview' ), 'permission_callback' => array( $this, 'can_manage' ), 'args' => array( 'post' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), ), ) ); // The pickable targets for the preview: the in-scope pages & posts, most // recently edited first, optionally filtered by a title search. Grouped by // post type and paginated per group — `type` + `offset` fetch a group's next // batch for the "Load more" button (see get_schema_targets()). register_rest_route( self::NAMESPACE, '/preview/targets', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_schema_targets' ), 'permission_callback' => array( $this, 'can_manage' ), 'args' => array( 'search' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'type' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key', ), 'offset' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), ), ) ); } /** * Permission gate. * * @return bool */ public function can_manage() { return current_user_can( 'manage_options' ); } /** * GET /settings. * * @return \WP_REST_Response */ public function get_settings() { return rest_ensure_response( array( 'settings' => $this->settings->all(), 'readiness' => ( new Readiness( $this->settings ) )->report(), ) ); } /** * POST /settings. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response */ public function save_settings( \WP_REST_Request $request ) { $input = $request->get_json_params(); if ( ! is_array( $input ) ) { $input = (array) $request->get_param( 'settings' ); } if ( isset( $input['settings'] ) && is_array( $input['settings'] ) ) { $input = $input['settings']; } $saved = $this->settings->update( (array) $input ); return rest_ensure_response( array( 'settings' => $saved, 'readiness' => ( new Readiness( $this->settings ) )->report(), 'saved' => true, ) ); } /** * POST /settings/reset — restore factory defaults. * * @return \WP_REST_Response */ public function reset_settings() { $defaults = $this->settings->reset(); return rest_ensure_response( array( 'settings' => $defaults, 'readiness' => ( new Readiness( $this->settings ) )->report(), 'reset' => true, ) ); } /** * POST /onboarding — mark the first-run setup wizard complete (or skipped) so * it never shows again. Stored as its own option, not inside settings, so a * factory reset doesn't re-trigger the wizard. * * @return \WP_REST_Response */ public function complete_onboarding() { update_option( 'agentimus_onboarded', AGENTIMUS_VERSION ); return rest_ensure_response( array( 'onboarded' => true ) ); } /** * GET /readiness. * * @return \WP_REST_Response */ public function get_readiness() { return rest_ensure_response( ( new Readiness( $this->settings ) )->report() ); } /** * GET /preview/schema — the JSON-LD document for the site or a chosen post. * * Returns the graph regardless of whether the front end is currently emitting * it (schema disabled, or an SEO plugin owns it): the point of a preview is to * show what WOULD ship. The `active`/`reason` fields let the UI explain when the * live `` is empty. An unpublished post (draft/pending/scheduled/private) * is previewed with its would-be per-post node — flagged by `postNote` as not yet * live — so the owner sees what publishing will emit. A password-protected post * is the one exception: its body never ships as schema, so it still yields only * the site-level nodes. `livePublic` marks whether the target is reachable at a * public URL right now, gating the URL-based validators. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function get_schema_preview( \WP_REST_Request $request ) { $schema = new Schema( $this->settings ); $seo = $schema->seo_plugin_active(); $enabled = $this->settings->enabled( 'enable_schema' ); $post_id = absint( $request->get_param( 'post' ) ); $post = null; $post_included = true; $post_note = ''; $live_public = true; // The site view is always reachable at a public URL. $target = array( 'type' => 'site', 'id' => 0, 'label' => __( 'Site-wide identity', 'agentimus' ), 'url' => home_url( '/' ), ); if ( $post_id > 0 ) { $post = get_post( $post_id ); if ( ! $post || ! in_array( $post->post_type, Content::post_types(), true ) ) { return new \WP_Error( 'agentimus_preview_not_found', __( 'That content is not available to preview.', 'agentimus' ), array( 'status' => 404 ) ); } $target = array( 'type' => 'post', 'id' => (int) $post->ID, 'label' => $this->preview_label( $post ), 'url' => (string) get_permalink( $post ), ); // Only a published, non-gated post is reachable at a public URL — the // precondition for the URL-based validators (a draft URL would 404). $live_public = ( 'publish' === $post->post_status && '' === (string) $post->post_password ); if ( '' !== (string) $post->post_password ) { // Never previewed: a gated body stays private even once published. $post_included = false; $post_note = __( 'This is password-protected, so its content is never exposed as schema — only the site-wide identity below.', 'agentimus' ); } elseif ( 'publish' !== $post->post_status ) { // Preview the node this post WILL emit once it's published — clearly // flagged as not-yet-live so the owner doesn't mistake it for live output. $post_note = __( 'Not published yet — this is a preview of the per-post schema that will ship once you publish. It isn’t in your live page’s yet.', 'agentimus' ); } } $doc = ( null === $post ) ? $schema->build_document( null, true ) : $schema->build_document( $post, false, true ); return rest_ensure_response( array( 'active' => (bool) ( $enabled && ! $seo ), 'reason' => ! $enabled ? 'disabled' : ( $seo ? 'seo_plugin' : 'ok' ), 'seoPlugin' => (bool) $seo, 'target' => $target, 'postIncluded' => (bool) $post_included, 'postNote' => $post_note, 'livePublic' => (bool) $live_public, 'graph' => $doc, // Pretty, slash-unescaped JSON for reading/validating. The live // escapes slashes to stay safe inside