registry = $registry; $this->catalog = $catalog; } /** * Registers the controller's REST routes. * * @return void */ public function register_routes(): void { register_rest_route( self::NAMESPACE, '/conditions', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_conditions' ), 'permission_callback' => array( $this, 'check_capability' ), ) ); register_rest_route( self::NAMESPACE, '/data', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_data' ), 'permission_callback' => array( $this, 'check_capability' ), ) ); register_rest_route( self::NAMESPACE, '/posts', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_posts' ), 'permission_callback' => array( $this, 'check_capability' ), 'args' => array( 'search' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ), 'type' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'default' => '', ), 'include' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ), ), ) ); } /** * Permission callback — the editor must be able to edit posts. * * @return bool */ public function check_capability(): bool { return current_user_can( 'edit_posts' ); } /** * Returns all registered condition definitions for the editor. * * @return WP_REST_Response */ public function get_conditions(): WP_REST_Response { $definitions = array(); foreach ( $this->registry->all() as $condition ) { $definitions[] = array( 'id' => $condition->id(), 'label' => $condition->label(), 'description' => $condition->description(), 'category' => $condition->category(), 'operators' => $this->catalog->operator_options( $condition->operators() ), 'defaultOperator' => $condition->default_operator(), 'valueSchema' => $condition->value_schema(), 'defaultValue' => $condition->default_value(), ); } return rest_ensure_response( $definitions ); } /** * Returns the translatable option lists consumed by the editor controls. * * @return WP_REST_Response */ public function get_data(): WP_REST_Response { return rest_ensure_response( array( 'roles' => $this->catalog->role_options(), 'postTypes' => $this->catalog->post_type_options(), 'weekdays' => $this->catalog->weekday_options(), 'archiveTypes' => $this->catalog->archive_type_options(), 'devices' => $this->catalog->device_options(), 'operators' => $this->catalog->operator_options( array_keys( $this->catalog->operator_labels() ) ), ) ); } /** * Searchable post/page selector for the Page / Post condition. * * Accepts `search` (title search), `type` (a single post type) and `include` * (a comma-separated id list to resolve specific items by id). * * Lists every published or private item the user may read, plus the draft, * pending and scheduled items they may edit, so a block can be targeted at * content before it goes live. * * @param WP_REST_Request $request Full request object. * @return WP_REST_Response */ public function get_posts( WP_REST_Request $request ): WP_REST_Response { $search = (string) $request->get_param( 'search' ); $type = (string) $request->get_param( 'type' ); $include = array_filter( array_map( 'absint', explode( ',', (string) $request->get_param( 'include' ) ) ) ); $post_types = $this->catalog->public_post_types(); if ( $type && in_array( $type, $post_types, true ) ) { $post_types = array( $type ); } /* * Queries run per post type: `perm` is only reliable for a single type. * With several, WP_Query checks the non-existent * `read_private_multiple_post_types` capability and limits private items * to the current author's own — even for administrators. Applying the * permission in SQL (not after the fact) keeps the row limit meaningful. */ $args = array( 'suppress_filters' => false, ); if ( ! empty( $include ) ) { $args['post__in'] = $include; $args['numberposts'] = count( $include ); } else { $args['s'] = $search; $args['numberposts'] = self::SEARCH_LIMIT; $args['orderby'] = 'title'; $args['order'] = 'ASC'; } $posts = array(); foreach ( $post_types as $post_type ) { foreach ( $this->status_queries( $post_type ) as $status_args ) { $posts = array_merge( $posts, get_posts( array_merge( $args, $status_args, array( 'post_type' => $post_type ) ) ) ); } } if ( ! empty( $include ) ) { $order = array_flip( $include ); usort( $posts, static fn( $a, $b ) => $order[ $a->ID ] <=> $order[ $b->ID ] ); } else { // Equal titles keep their merge order, as PHP 8's stable sort does; PHP 7.4's is not stable. $position = array_flip( array_map( static fn( $post ) => $post->ID, $posts ) ); usort( $posts, static function ( $a, $b ) use ( $position ): int { $order = strnatcasecmp( $a->post_title, $b->post_title ); return 0 !== $order ? $order : $position[ $a->ID ] <=> $position[ $b->ID ]; } ); $posts = array_slice( $posts, 0, self::SEARCH_LIMIT ); } $result = array(); foreach ( $posts as $post ) { $type_object = get_post_type_object( $post->post_type ); $status_object = get_post_status_object( $post->post_status ); $result[] = array( 'id' => (int) $post->ID, // Plain text: the editor renders it as text and filters suggestions by it. 'title' => html_entity_decode( wp_strip_all_tags( get_the_title( $post ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), 'type' => $post->post_type, 'typeLabel' => $type_object ? (string) $type_object->labels->singular_name : $post->post_type, 'status' => $post->post_status, 'statusLabel' => $status_object ? (string) $status_object->label : $post->post_status, 'edit' => get_edit_post_link( $post, 'raw' ), ); } return rest_ensure_response( $result ); } /** * Builds the status/permission argument sets to query for one post type. * * Published and private items are listed when the user may read them, and * draft, pending and scheduled items when the user may edit them — the rule * the admin post lists apply. A user who may edit others' posts of the type * may edit every unpublished item, so a single query under `perm => readable` * covers all statuses (it still limits private items to those they may * read). Anyone else gets a second query under `perm => editable`, which * WP_Query restricts to their own items. * * @param string $post_type Post type slug. * @return array */ private function status_queries( string $post_type ): array { $type_object = get_post_type_object( $post_type ); if ( $type_object && current_user_can( $type_object->cap->edit_others_posts ) ) { return array( array( 'post_status' => array( 'publish', 'private', 'draft', 'pending', 'future' ), 'perm' => 'readable', ), ); } return array( array( 'post_status' => array( 'publish', 'private' ), 'perm' => 'readable', ), array( 'post_status' => array( 'draft', 'pending', 'future' ), 'perm' => 'editable', ), ); } }