id = 'betterdocs/get-doc'; $this->label = __( 'Get doc', 'betterdocs' ); $this->description = __( 'Read one BetterDocs doc by id or slug: its raw block content, its rendered HTML, its categories, tags and knowledge bases, its view and reaction counts, and which FAQ groups its FAQ blocks show.', 'betterdocs' ); $this->capability = 'edit_docs'; } /** * @since 4.9.0 * * @return array */ public function get_annotations() { return [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, 'priority' => 1.5, 'openWorldHint' => false ]; } /** * @since 4.9.0 * * @return array */ public function get_input_schema() { return [ 'type' => 'object', 'additionalProperties' => false, 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => __( 'The doc id. Give this or slug.', 'betterdocs' ) ], 'slug' => [ 'type' => 'string', 'description' => __( 'The doc slug, as it appears in the URL. Give this or id.', 'betterdocs' ) ], 'context' => [ 'type' => 'string', 'enum' => [ 'view', 'edit' ], 'default' => 'edit', 'description' => __( 'edit (default) includes the raw block content, and quietly falls back to view for a doc you may read but not edit; view returns only the rendered HTML.', 'betterdocs' ) ] ], 'default' => [] ]; } /** * @since 4.9.0 * * @return array */ public function get_output_schema() { return [ 'type' => 'object', 'properties' => array_merge( self::doc_summary_schema(), [ 'content' => [ 'type' => 'object', 'properties' => [ 'raw' => [ 'type' => 'string' ], 'rendered' => [ 'type' => 'string' ] ] ], 'total_views' => [ 'type' => 'integer' ], 'reactions' => [ 'type' => 'object', 'properties' => [ 'happy' => [ 'type' => 'integer' ], 'normal' => [ 'type' => 'integer' ], 'sad' => [ 'type' => 'integer' ] ] ], 'faq_groups' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ] ] ) ]; } /** * @since 4.9.0 * * @param array $input Validated input. * @return array|\WP_Error */ public function execute( $input ) { $context = isset( $input['context'] ) ? (string) $input['context'] : 'edit'; $id = isset( $input['id'] ) ? (int) $input['id'] : 0; $slug = isset( $input['slug'] ) ? trim( (string) $input['slug'] ) : ''; if ( $id <= 0 && '' === $slug ) { return AbilityError::invalid_input( 'id', __( 'Give either id or slug.', 'betterdocs' ) ); } if ( $id <= 0 ) { $id = $this->id_for_slug( $slug ); if ( is_wp_error( $id ) ) { return $id; } } $post = $this->require_doc( $id ); if ( is_wp_error( $post ) ) { return $post; } if ( ! current_user_can( 'read_post', $post->ID ) ) { return AbilityError::capability_missing( 'read_private_docs', sprintf( /* translators: %d: doc id. */ __( 'read doc #%d', 'betterdocs' ), (int) $post->ID ) ); } // Edit context is refused outright for a doc this user may read but not // edit, so ask for it only when it can be given. if ( 'edit' === $context && ! current_user_can( 'edit_post', $post->ID ) ) { $context = 'view'; } $doc = $this->dispatch( 'GET', '/docs/' . (int) $post->ID, [ 'context' => $context ], 'wp/v2' ); if ( is_wp_error( $doc ) ) { return $this->map_rest_error( $doc, sprintf( /* translators: %d: doc id. */ __( 'read doc #%d', 'betterdocs' ), (int) $post->ID ), $post ); } $doc = (array) $doc; return array_merge( $this->doc_summary( $doc ), [ 'content' => [ // Empty in view context: WordPress does not hand the stored // markup to someone who cannot edit the post, and neither // should this. 'raw' => isset( $doc['content']['raw'] ) ? (string) $doc['content']['raw'] : '', 'rendered' => isset( $doc['content']['rendered'] ) ? (string) $doc['content']['rendered'] : '' ], 'total_views' => isset( $doc['total_views'] ) ? (int) $doc['total_views'] : 0, 'reactions' => $this->reactions( isset( $doc['reactions'] ) ? (array) $doc['reactions'] : [] ), // From the post, not the response: the block attributes are the // same whichever context was granted, and reporting no FAQ // groups because the context was narrowed would be a wrong // answer rather than a withheld one. 'faq_groups' => $this->faq_group_ids( (string) $post->post_content ) ] ); } /** * The doc id behind a slug. * * `status => any` because an agent asked to finish a draft knows its slug, * not its id; the ability's own `edit_docs` gate and the `read_post` check * below are what decide whether it may see it. * * @since 4.9.0 * * @param string $slug Doc slug. * @return int|\WP_Error */ protected function id_for_slug( $slug ) { $found = $this->dispatch( 'GET', '/docs', [ 'slug' => $slug, 'status' => 'any', 'per_page' => 1, 'context' => 'edit' ], 'wp/v2' ); if ( is_wp_error( $found ) ) { return $this->map_rest_error( $found, __( 'look a doc up by slug', 'betterdocs' ) ); } if ( ! is_array( $found ) || empty( $found[0]['id'] ) ) { return AbilityError::not_found( 'doc', $slug ); } return (int) $found[0]['id']; } /** * Reaction counts as integers, with every key present. * * @since 4.9.0 * * @param array $reactions What the REST field returned. * @return array */ protected function reactions( array $reactions ) { return [ 'happy' => isset( $reactions['happy'] ) ? (int) $reactions['happy'] : 0, 'normal' => isset( $reactions['normal'] ) ? (int) $reactions['normal'] : 0, 'sad' => isset( $reactions['sad'] ) ? (int) $reactions['sad'] : 0 ]; } /** * FAQ group ids the doc's FAQ blocks include. * * @since 4.9.0 * * @param string $raw Raw block content. * @return int[] */ protected function faq_group_ids( $raw ) { $ids = []; foreach ( BlockBuilder::find_faq_blocks( $raw ) as $block ) { foreach ( $block['include'] as $group ) { if ( isset( $group['id'] ) ) { $ids[] = (int) $group['id']; } } } return array_values( array_unique( $ids ) ); } }