'GET', 'callback' => array( __CLASS__, 'health' ), 'permission_callback' => $auth ) ); register_rest_route( self::NS, '/info', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'info' ), 'permission_callback' => $auth ) ); register_rest_route( self::NS, '/posts', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'create' ), 'permission_callback' => $auth ) ); register_rest_route( self::NS, '/meta', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'meta' ), 'permission_callback' => $auth ) ); register_rest_route( self::NS, '/resolve', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'resolve' ), 'permission_callback' => $auth ) ); register_rest_route( self::NS, '/connect', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'connect' ), 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); register_rest_route( self::NS, '/posts/(?P\d+)/(?Ppublish|unpublish|update)', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'change' ), 'permission_callback' => $auth ) ); } /** * Connect with a one-time code, the same as pasting it on Settings → Gralus. * Body: { code }. */ public static function connect( $request ) { $result = Gralus_Connection::connect_with_code( (string) $request->get_param( 'code' ) ); if ( is_wp_error( $result ) ) { return new WP_Error( $result->get_error_code(), $result->get_error_message(), array( 'status' => 400 ) ); } return array( 'ok' => true, 'pluginVersion' => GRALUS_VERSION, ); } /** True when the URL is this site's home page (with or without a trailing slash or query). */ private static function is_home_url( $url ) { $home = untrailingslashit( home_url( '/' ) ); $url = untrailingslashit( strtok( $url, '?' ) ); return $url === $home || $url === set_url_scheme( $home, 'http' ) || $url === set_url_scheme( $home, 'https' ); } /** The public post type whose archive lives at this URL, or ''. */ private static function archive_type_for_url( $url ) { $want = untrailingslashit( set_url_scheme( strtok( $url, '?' ), 'https' ) ); foreach ( get_post_types( array( 'public' => true, 'has_archive' => true ), 'names' ) as $type ) { $link = get_post_type_archive_link( $type ); if ( $link && untrailingslashit( set_url_scheme( $link, 'https' ) ) === $want ) { return $type; } } return ''; } /** Find the post or page behind a URL on this site (a static front page included). */ public static function post_id_for_url( $url ) { $id = url_to_postid( $url ); if ( ! $id && self::is_home_url( $url ) && 'page' === get_option( 'show_on_front' ) ) { $id = (int) get_option( 'page_on_front' ); } return (int) $id; } /** * Set the SEO title and/or description of an existing page by URL. * Body: { url, seoTitle?: string|null, seoDescription?: string|null }. Returns the previous values for undo. */ public static function meta( WP_REST_Request $request ) { $p = $request->get_json_params(); $url = isset( $p['url'] ) ? esc_url_raw( $p['url'] ) : ''; $id = $url ? self::post_id_for_url( $url ) : 0; if ( ! $id && $url && self::is_home_url( $url ) ) { // The front page lists the latest posts, so there is no post to attach the values to: store them for the home page itself. $before = Gralus_Seo_Meta::get_home(); Gralus_Seo_Meta::set_home_exact( array_key_exists( 'seoTitle', $p ) ? $p['seoTitle'] : null, array_key_exists( 'seoDescription', $p ) ? $p['seoDescription'] : null ); return array( 'ok' => true, 'id' => 0, 'url' => home_url( '/' ), 'previous' => array( 'seoTitle' => $before['title'], 'seoDescription' => $before['description'] ), 'current' => Gralus_Seo_Meta::get_home(), ); } $post = $id ? get_post( $id ) : null; if ( ! $post ) { // A post type's archive page, such as /case-study/. $type = $url ? self::archive_type_for_url( $url ) : ''; if ( $type ) { $before = Gralus_Seo_Meta::get_archive( $type ); Gralus_Seo_Meta::set_archive_exact( $type, array_key_exists( 'seoTitle', $p ) ? $p['seoTitle'] : null, array_key_exists( 'seoDescription', $p ) ? $p['seoDescription'] : null ); return array( 'ok' => true, 'id' => 0, 'url' => get_post_type_archive_link( $type ), 'previous' => array( 'seoTitle' => $before['title'], 'seoDescription' => $before['description'] ), 'current' => Gralus_Seo_Meta::get_archive( $type ), ); } } // Posts, pages and any public custom post type (case studies, services, products…). if ( ! $post || 'attachment' === $post->post_type || ! is_post_type_viewable( $post->post_type ) ) { return new WP_Error( 'gralus_missing', 'No post or page was found at that address.', array( 'status' => 404 ) ); } $before = Gralus_Seo_Meta::get( $id ); Gralus_Seo_Meta::set_exact( $id, array_key_exists( 'seoTitle', $p ) ? $p['seoTitle'] : null, array_key_exists( 'seoDescription', $p ) ? $p['seoDescription'] : null ); return array( 'ok' => true, 'id' => $id, 'url' => get_permalink( $id ), 'previous' => array( 'seoTitle' => $before['title'], 'seoDescription' => $before['description'] ), 'current' => Gralus_Seo_Meta::get( $id ), ); } /** * Which of these addresses can Gralus change the SEO title and description of? * Body: { urls: string[] } (max 200). Returns { results: { [url]: { editable, kind } } }. * kind: post (any public post type), archive (post type archive), home (latest-posts front page), none (not WordPress). */ public static function resolve( WP_REST_Request $request ) { $p = $request->get_json_params(); $urls = isset( $p['urls'] ) && is_array( $p['urls'] ) ? array_slice( $p['urls'], 0, 200 ) : array(); $out = array(); foreach ( $urls as $raw ) { $url = esc_url_raw( (string) $raw ); $kind = 'none'; if ( $url ) { $id = self::post_id_for_url( $url ); $post = $id ? get_post( $id ) : null; if ( $post && 'attachment' !== $post->post_type && is_post_type_viewable( $post->post_type ) ) { $kind = 'post'; } elseif ( ! $id && self::archive_type_for_url( $url ) ) { $kind = 'archive'; } elseif ( ! $id && self::is_home_url( $url ) ) { $kind = 'home'; } } $out[ (string) $raw ] = array( 'editable' => 'none' !== $kind, 'kind' => $kind ); } return array( 'results' => $out ); } public static function health() { return array( 'ok' => true, 'pluginVersion' => GRALUS_VERSION, 'wpVersion' => get_bloginfo( 'version' ), ); } public static function info() { return Gralus_Site_Info::collect(); } /** The user who appears as author: the one chosen in Gralus, else the first administrator. */ private static function author_id() { return Gralus_Author::author_id(); } private static function result( $post_id ) { $post = get_post( $post_id ); return array( 'ok' => true, 'id' => (int) $post_id, 'status' => $post->post_status, 'url' => get_permalink( $post_id ), 'previewUrl' => get_preview_post_link( $post_id ), 'editUrl' => admin_url( 'post.php?post=' . (int) $post_id . '&action=edit' ), ); } /** * Create a draft post or page. * Body: { type: post|page, title, content, excerpt?, slug?, categories?: int[], seoTitle?, seoDescription?, faq?, design?, layout?: "full" } */ public static function create( WP_REST_Request $request ) { $p = $request->get_json_params(); $type = ( isset( $p['type'] ) && 'page' === $p['type'] ) ? 'page' : 'post'; if ( empty( $p['title'] ) || ! isset( $p['content'] ) ) { return new WP_Error( 'gralus_invalid', 'Title and content are required.', array( 'status' => 400 ) ); } $data = array( 'post_type' => $type, 'post_status' => 'draft', 'post_title' => sanitize_text_field( $p['title'] ), 'post_content' => wp_kses_post( $p['content'] ), 'post_excerpt' => isset( $p['excerpt'] ) ? sanitize_textarea_field( $p['excerpt'] ) : '', 'post_author' => self::author_id(), ); if ( ! empty( $p['slug'] ) ) { $data['post_name'] = sanitize_title( $p['slug'] ); } if ( 'post' === $type && ! empty( $p['categories'] ) && is_array( $p['categories'] ) ) { $data['post_category'] = array_map( 'intval', $p['categories'] ); } $id = wp_insert_post( wp_slash( $data ), true ); if ( is_wp_error( $id ) ) { return new WP_Error( 'gralus_insert', $id->get_error_message(), array( 'status' => 500 ) ); } update_post_meta( $id, self::OWNED_KEY, 1 ); Gralus_Seo_Meta::set( $id, $p['seoTitle'] ?? '', $p['seoDescription'] ?? '' ); if ( array_key_exists( 'faq', $p ) ) { Gralus_Sections::save_faq( $id, $p['faq'] ); } if ( array_key_exists( 'design', $p ) || array_key_exists( 'layout', $p ) ) { Gralus_Sections::save_look( $id, $p['design'] ?? null, $p['layout'] ?? null ); } return self::result( $id ); } /** * Publish, unpublish (back to draft) or update a post this plugin created. * Update keeps a WordPress revision so the change can be undone. */ public static function change( WP_REST_Request $request ) { $id = (int) $request['id']; $action = $request['action']; $post = get_post( $id ); if ( ! $post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) { return new WP_Error( 'gralus_missing', 'Post not found.', array( 'status' => 404 ) ); } if ( 'update' !== $action && ! get_post_meta( $id, self::OWNED_KEY, true ) ) { return new WP_Error( 'gralus_not_owned', 'Gralus can only publish or unpublish content it created.', array( 'status' => 403 ) ); } if ( 'publish' === $action ) { $r = wp_update_post( array( 'ID' => $id, 'post_status' => 'publish' ), true ); } elseif ( 'unpublish' === $action ) { $r = wp_update_post( array( 'ID' => $id, 'post_status' => 'draft' ), true ); } else { $p = $request->get_json_params(); $data = array( 'ID' => $id ); if ( isset( $p['title'] ) ) { $data['post_title'] = sanitize_text_field( $p['title'] ); } if ( isset( $p['content'] ) ) { $data['post_content'] = wp_kses_post( $p['content'] ); } if ( isset( $p['excerpt'] ) ) { $data['post_excerpt'] = sanitize_textarea_field( $p['excerpt'] ); } if ( function_exists( 'wp_save_post_revision' ) ) { wp_save_post_revision( $id ); } $r = wp_update_post( wp_slash( $data ), true ); if ( ! is_wp_error( $r ) && ( isset( $p['seoTitle'] ) || isset( $p['seoDescription'] ) ) ) { Gralus_Seo_Meta::set( $id, $p['seoTitle'] ?? '', $p['seoDescription'] ?? '' ); } if ( ! is_wp_error( $r ) && array_key_exists( 'faq', $p ) ) { Gralus_Sections::save_faq( $id, $p['faq'] ); } if ( ! is_wp_error( $r ) && ( array_key_exists( 'design', $p ) || array_key_exists( 'layout', $p ) ) ) { Gralus_Sections::save_look( $id, $p['design'] ?? null, $p['layout'] ?? null ); } } if ( is_wp_error( $r ) ) { return new WP_Error( 'gralus_update', $r->get_error_message(), array( 'status' => 500 ) ); } return self::result( $id ); } }