| 1 |
<?php |
| 2 |
/** |
| 3 |
* List FAQ groups ability. |
| 4 |
* |
| 5 |
* @package BetterDocs |
| 6 |
* @since 4.9.0 |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace WPDeveloper\BetterDocs\Abilities\Faq; |
| 10 |
|
| 11 |
if ( ! defined( 'ABSPATH' ) ) { |
| 12 |
exit; // Exit if accessed directly. |
| 13 |
} |
| 14 |
|
| 15 |
use WPDeveloper\BetterDocs\Abilities\AbilityBase; |
| 16 |
use WPDeveloper\BetterDocs\Abilities\Traits\ShapesFAQs; |
| 17 |
|
| 18 |
/** |
| 19 |
* What FAQ groups exist, how many questions are in each, and which of them the |
| 20 |
* front end is actually showing. |
| 21 |
* |
| 22 |
* The tool to call before creating a group or attaching one to a doc, because |
| 23 |
* it is what turns "the billing FAQ" into an id instead of a second group with |
| 24 |
* a near-identical name. |
| 25 |
* |
| 26 |
* **Order is the site's own.** `FAQBuilder::faq_category_orderby_meta()` hooks |
| 27 |
* `rest_betterdocs_faq_category_query` and sets `orderby`/`order` from the FAQ |
| 28 |
* Builder's header preference (the `betterdocs_faq_order` option) on every REST |
| 29 |
* request for this taxonomy — measured, not assumed. So this tool takes no |
| 30 |
* `orderby`: the list comes back in the order the FAQ Builder and the front end |
| 31 |
* show it, which is the order an agent should reason about anyway. |
| 32 |
* |
| 33 |
* @since 4.9.0 |
| 34 |
*/ |
| 35 |
class ListFAQGroups extends AbilityBase { |
| 36 |
|
| 37 |
use ShapesFAQs; |
| 38 |
|
| 39 |
/** |
| 40 |
* @since 4.9.0 |
| 41 |
*/ |
| 42 |
public function __construct() { |
| 43 |
$this->id = 'betterdocs/list-faq-groups'; |
| 44 |
$this->label = __( 'List FAQ groups', 'betterdocs' ); |
| 45 |
$this->description = __( 'List BetterDocs FAQ groups with their question counts and whether each is published or draft, in the order the site shows them. Call this before creating a group so you reuse what exists.', 'betterdocs' ); |
| 46 |
$this->capability = 'edit_others_docs'; |
| 47 |
} |
| 48 |
|
| 49 |
/** |
| 50 |
* @since 4.9.0 |
| 51 |
* |
| 52 |
* @return array |
| 53 |
*/ |
| 54 |
public function get_annotations() { |
| 55 |
return [ |
| 56 |
'readonly' => true, |
| 57 |
'destructive' => false, |
| 58 |
'idempotent' => true, |
| 59 |
'priority' => 1.5, |
| 60 |
'openWorldHint' => false |
| 61 |
]; |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* @since 4.9.0 |
| 66 |
* |
| 67 |
* @return array |
| 68 |
*/ |
| 69 |
public function get_input_schema() { |
| 70 |
return [ |
| 71 |
'type' => 'object', |
| 72 |
'additionalProperties' => false, |
| 73 |
'properties' => [ |
| 74 |
'search' => [ |
| 75 |
'type' => 'string', |
| 76 |
'description' => __( 'Free-text search over group names and slugs.', 'betterdocs' ) |
| 77 |
], |
| 78 |
'status' => [ |
| 79 |
'type' => 'string', |
| 80 |
'enum' => [ 'any', 'publish', 'draft' ], |
| 81 |
'default' => 'any', |
| 82 |
'description' => __( 'publish lists only the groups the front end shows, draft only the hidden ones. Defaults to any.', 'betterdocs' ) |
| 83 |
], |
| 84 |
'page' => [ |
| 85 |
'type' => 'integer', |
| 86 |
'minimum' => 1, |
| 87 |
'default' => 1 |
| 88 |
], |
| 89 |
'per_page' => [ |
| 90 |
'type' => 'integer', |
| 91 |
'minimum' => 1, |
| 92 |
'maximum' => 100, |
| 93 |
'default' => 50, |
| 94 |
'description' => __( 'Groups per page, 1–100.', 'betterdocs' ) |
| 95 |
] |
| 96 |
], |
| 97 |
'default' => [] |
| 98 |
]; |
| 99 |
} |
| 100 |
|
| 101 |
/** |
| 102 |
* @since 4.9.0 |
| 103 |
* |
| 104 |
* @return array |
| 105 |
*/ |
| 106 |
public function get_output_schema() { |
| 107 |
return [ |
| 108 |
'type' => 'object', |
| 109 |
'properties' => [ |
| 110 |
'items' => [ |
| 111 |
'type' => 'array', |
| 112 |
'items' => [ |
| 113 |
'type' => 'object', |
| 114 |
'properties' => self::group_shape_schema() |
| 115 |
] |
| 116 |
], |
| 117 |
'total' => [ 'type' => 'integer' ], |
| 118 |
'total_pages' => [ 'type' => 'integer' ], |
| 119 |
'page' => [ 'type' => 'integer' ], |
| 120 |
'per_page' => [ 'type' => 'integer' ] |
| 121 |
] |
| 122 |
]; |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* @since 4.9.0 |
| 127 |
* |
| 128 |
* @param array $input Validated input. |
| 129 |
* @return array|\WP_Error |
| 130 |
*/ |
| 131 |
public function execute( $input ) { |
| 132 |
$page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1; |
| 133 |
$per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 50; |
| 134 |
$status = isset( $input['status'] ) ? (string) $input['status'] : 'any'; |
| 135 |
|
| 136 |
$params = [ |
| 137 |
// View, not edit: `WP_REST_Terms_Controller` refuses `context=edit` |
| 138 |
// without the taxonomy's `edit_terms` capability, and this |
| 139 |
// taxonomy's is `edit_doc_terms` — which the FAQ tools deliberately |
| 140 |
// do not require (ADR-005). Term meta, the count and the draft count |
| 141 |
// are all present in view context anyway. |
| 142 |
'context' => 'view', |
| 143 |
'hide_empty' => false, |
| 144 |
'page' => $page, |
| 145 |
'per_page' => $per_page |
| 146 |
]; |
| 147 |
|
| 148 |
if ( isset( $input['search'] ) && '' !== (string) $input['search'] ) { |
| 149 |
$params['search'] = (string) $input['search']; |
| 150 |
} |
| 151 |
|
| 152 |
$response = $this->dispatch_groups( $params, $status ); |
| 153 |
|
| 154 |
if ( $response->is_error() ) { |
| 155 |
return $this->map_faq_error( $response->as_error(), __( 'list FAQ groups', 'betterdocs' ) ); |
| 156 |
} |
| 157 |
|
| 158 |
$headers = $response->get_headers(); |
| 159 |
$items = []; |
| 160 |
|
| 161 |
foreach ( (array) $response->get_data() as $term ) { |
| 162 |
$items[] = $this->group_shape( (array) $term ); |
| 163 |
} |
| 164 |
|
| 165 |
return [ |
| 166 |
'items' => $items, |
| 167 |
'total' => isset( $headers['X-WP-Total'] ) ? (int) $headers['X-WP-Total'] : count( $items ), |
| 168 |
'total_pages' => isset( $headers['X-WP-TotalPages'] ) ? (int) $headers['X-WP-TotalPages'] : 1, |
| 169 |
'page' => $page, |
| 170 |
'per_page' => $per_page |
| 171 |
]; |
| 172 |
} |
| 173 |
|
| 174 |
/** |
| 175 |
* Run the collection request, narrowing it to one `status` when asked. |
| 176 |
* |
| 177 |
* The group's published/hidden state is a term meta the REST controller |
| 178 |
* knows nothing about, so the filter goes in as a `meta_query` on this one |
| 179 |
* request — identity-compared, so nothing else in the page is affected — and |
| 180 |
* `X-WP-Total` then counts the same set the items came from. Filtering the |
| 181 |
* page after the fact would have made `total` a lie. |
| 182 |
* |
| 183 |
* `draft` deliberately also matches a group with **no** `status` meta at |
| 184 |
* all: the front end (`Query::faq_terms_query_args()`) shows only `status = |
| 185 |
* 1`, so a group that predates the meta is hidden, and reporting it as |
| 186 |
* published would be wrong. |
| 187 |
* |
| 188 |
* @since 4.9.0 |
| 189 |
* |
| 190 |
* @param array $params Query parameters. |
| 191 |
* @param string $status `any`, `publish` or `draft`. |
| 192 |
* @return \WP_REST_Response |
| 193 |
*/ |
| 194 |
protected function dispatch_groups( array $params, $status ) { |
| 195 |
$request = new \WP_REST_Request( 'GET', '/wp/v2/' . self::GROUP_TAXONOMY ); |
| 196 |
|
| 197 |
$request->set_header( 'Content-Type', 'application/json' ); |
| 198 |
$request->set_query_params( $params ); |
| 199 |
|
| 200 |
if ( 'publish' !== $status && 'draft' !== $status ) { |
| 201 |
return rest_do_request( $request ); |
| 202 |
} |
| 203 |
|
| 204 |
$meta_query = 'publish' === $status |
| 205 |
? [ |
| 206 |
[ |
| 207 |
'key' => 'status', |
| 208 |
'value' => '1', |
| 209 |
'compare' => '=' |
| 210 |
] |
| 211 |
] |
| 212 |
: [ |
| 213 |
'relation' => 'OR', |
| 214 |
[ |
| 215 |
'key' => 'status', |
| 216 |
'value' => '1', |
| 217 |
'compare' => '!=' |
| 218 |
], |
| 219 |
[ |
| 220 |
'key' => 'status', |
| 221 |
'compare' => 'NOT EXISTS' |
| 222 |
] |
| 223 |
]; |
| 224 |
|
| 225 |
$narrow = function ( $args, $filtered_request ) use ( $request, $meta_query ) { |
| 226 |
if ( $filtered_request !== $request ) { |
| 227 |
return $args; |
| 228 |
} |
| 229 |
|
| 230 |
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- the group's published state is a term meta; filtering on it is the feature. |
| 231 |
$args['meta_query'] = isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) && ! empty( $args['meta_query'] ) |
| 232 |
? [ |
| 233 |
'relation' => 'AND', |
| 234 |
$args['meta_query'], |
| 235 |
$meta_query |
| 236 |
] |
| 237 |
: $meta_query; |
| 238 |
|
| 239 |
return $args; |
| 240 |
}; |
| 241 |
|
| 242 |
add_filter( 'rest_' . self::GROUP_TAXONOMY . '_query', $narrow, 20, 2 ); |
| 243 |
|
| 244 |
$response = rest_do_request( $request ); |
| 245 |
|
| 246 |
remove_filter( 'rest_' . self::GROUP_TAXONOMY . '_query', $narrow, 20 ); |
| 247 |
|
| 248 |
return $response; |
| 249 |
} |
| 250 |
} |
| 251 |
|