| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ability base class. |
| 4 |
* |
| 5 |
* @package BetterDocs |
| 6 |
* @since 4.9.0 |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace WPDeveloper\BetterDocs\Abilities; |
| 10 |
|
| 11 |
if ( ! defined( 'ABSPATH' ) ) { |
| 12 |
exit; // Exit if accessed directly. |
| 13 |
} |
| 14 |
|
| 15 |
/** |
| 16 |
* Base implementation every BetterDocs ability extends. |
| 17 |
* |
| 18 |
* An ability declares what it is (id, label, description), who may call it |
| 19 |
* (one capability), what it takes and returns (JSON Schema), and what it does |
| 20 |
* (`execute()`). The Abilities API registers it; the MCP server reads the same |
| 21 |
* registry as its tool catalog, so one definition serves both. |
| 22 |
* |
| 23 |
* Most abilities reuse BetterDocs' own REST controllers through |
| 24 |
* {@see self::dispatch()} rather than reimplementing doc/term/FAQ logic. That |
| 25 |
* keeps validation, sanitisation and side effects in one place, and means REST |
| 26 |
* fixes reach MCP for free. |
| 27 |
* |
| 28 |
* @since 4.9.0 |
| 29 |
*/ |
| 30 |
abstract class AbilityBase { |
| 31 |
|
| 32 |
/** |
| 33 |
* Default REST namespace {@see self::dispatch()} targets. |
| 34 |
* |
| 35 |
* @since 4.9.0 |
| 36 |
*/ |
| 37 |
protected const NS = 'betterdocs/v1'; |
| 38 |
|
| 39 |
// The six constants below belong to `Traits\ShapesTerms` and |
| 40 |
// `Traits\ShapesFAQs` by subject, and live here because **PHP 7.4 traits |
| 41 |
// cannot carry constants** — that is PHP 8.2 syntax and this plugin's floor is |
| 42 |
// 7.4. Every `self::` use of them sits in a class that extends this one, so |
| 43 |
// each reference still resolves, through inheritance instead of the trait. |
| 44 |
|
| 45 |
/** |
| 46 |
* The taxonomies the Terms abilities address. Knowledge bases are |
| 47 |
* deliberately not among them: they are the Pro family's own four tools |
| 48 |
* (ADR-003). `glossaries` is here rather than in a tool family of its own |
| 49 |
* because it is an ordinary hierarchical taxonomy on `docs` carrying the |
| 50 |
* same four capabilities as the other two (ADR-061); it exists only while |
| 51 |
* `enable_glossaries` is on, which {@see Traits\ResolvesTerms::taxonomy_available()} |
| 52 |
* answers for. |
| 53 |
* |
| 54 |
* @since 4.9.0 |
| 55 |
*/ |
| 56 |
protected const TERM_TAXONOMIES = [ 'doc_category', 'doc_tag', 'glossaries' ]; |
| 57 |
|
| 58 |
/** |
| 59 |
* The setting that registers the glossary taxonomy. A Free setting, so its |
| 60 |
* absence is a `setting_disabled` refusal and never a Pro state (ADR-061). |
| 61 |
* |
| 62 |
* @since 4.9.0 |
| 63 |
*/ |
| 64 |
protected const GLOSSARY_SETTING = 'enable_glossaries'; |
| 65 |
|
| 66 |
/** |
| 67 |
* The glossary taxonomy's own description meta. |
| 68 |
* |
| 69 |
* BetterDocs drains the term's native `description` into this key and blanks |
| 70 |
* the column (`Core\Glossaries::update_glossary_term()`), and both the admin |
| 71 |
* screen and the A–Z front end read this first, so for `glossaries` the |
| 72 |
* tools' `description` field is this meta (ADR-061). |
| 73 |
* |
| 74 |
* @since 4.9.0 |
| 75 |
*/ |
| 76 |
protected const GLOSSARY_DESCRIPTION_META = 'glossary_term_description'; |
| 77 |
|
| 78 |
/** |
| 79 |
* The doc-category term meta recording knowledge-base membership, registered |
| 80 |
* for REST. |
| 81 |
* |
| 82 |
* @since 4.9.0 |
| 83 |
*/ |
| 84 |
protected const KB_META_KEY = 'doc_category_knowledge_base'; |
| 85 |
|
| 86 |
/** |
| 87 |
* The FAQ group taxonomy the FAQ abilities address. The WooCommerce Product |
| 88 |
* FAQ groups (`betterdocs_product_faq_category`) share the post type but are a |
| 89 |
* separate taxonomy and a separate feature; they are not exposed here. |
| 90 |
* |
| 91 |
* @since 4.9.0 |
| 92 |
*/ |
| 93 |
protected const GROUP_TAXONOMY = 'betterdocs_faq_category'; |
| 94 |
|
| 95 |
/** |
| 96 |
* The FAQ post type. |
| 97 |
* |
| 98 |
* @since 4.9.0 |
| 99 |
*/ |
| 100 |
protected const FAQ_POST_TYPE = 'betterdocs_faq'; |
| 101 |
|
| 102 |
/** |
| 103 |
* REST namespace of the FAQ Builder routes. `betterdocs`, with no version |
| 104 |
* segment — `FAQBuilder::$namespace` has been that since 1.0. |
| 105 |
* |
| 106 |
* @since 4.9.0 |
| 107 |
*/ |
| 108 |
protected const FAQ_NS = 'betterdocs'; |
| 109 |
|
| 110 |
/** |
| 111 |
* Post statuses an FAQ may be written with. |
| 112 |
* |
| 113 |
* @since 4.9.0 |
| 114 |
*/ |
| 115 |
protected const FAQ_STATUSES = [ 'publish', 'draft', 'pending', 'private' ]; |
| 116 |
|
| 117 |
/** |
| 118 |
* Unique ability identifier, e.g. `betterdocs/create-doc`. |
| 119 |
* |
| 120 |
* @since 4.9.0 |
| 121 |
* |
| 122 |
* @var string |
| 123 |
*/ |
| 124 |
protected $id = ''; |
| 125 |
|
| 126 |
/** |
| 127 |
* Human-readable label. Becomes the MCP tool `title`. |
| 128 |
* |
| 129 |
* @since 4.9.0 |
| 130 |
* |
| 131 |
* @var string |
| 132 |
*/ |
| 133 |
protected $label = ''; |
| 134 |
|
| 135 |
/** |
| 136 |
* Static description. {@see self::describe()} may vary it per Pro state. |
| 137 |
* |
| 138 |
* @since 4.9.0 |
| 139 |
* |
| 140 |
* @var string |
| 141 |
*/ |
| 142 |
protected $description = ''; |
| 143 |
|
| 144 |
/** |
| 145 |
* Ability category slug. |
| 146 |
* |
| 147 |
* @since 4.9.0 |
| 148 |
* |
| 149 |
* @var string |
| 150 |
*/ |
| 151 |
protected $category = AbilitiesRegistrar::CATEGORY; |
| 152 |
|
| 153 |
/** |
| 154 |
* Capability that gates this ability. **Subclasses must set one** — there is |
| 155 |
* deliberately no default, so an ability cannot ship ungated by omission. |
| 156 |
* |
| 157 |
* There is deliberately no blanket `manage_options` gate. |
| 158 |
* Each ability declares the capability that already governs its feature |
| 159 |
* (`edit_docs`, `manage_doc_terms`, `edit_docs_settings`, …) so an editor or |
| 160 |
* author reaches exactly what the WordPress admin already lets them reach. |
| 161 |
* |
| 162 |
* @since 4.9.0 |
| 163 |
* |
| 164 |
* @var string |
| 165 |
*/ |
| 166 |
protected $capability = ''; |
| 167 |
|
| 168 |
/** |
| 169 |
* Whether the feature behind this ability needs BetterDocs Pro. |
| 170 |
* |
| 171 |
* @since 4.9.0 |
| 172 |
* |
| 173 |
* @var bool |
| 174 |
*/ |
| 175 |
protected $requires_pro = false; |
| 176 |
|
| 177 |
/** |
| 178 |
* JSON Schema for the ability's input. |
| 179 |
* |
| 180 |
* @since 4.9.0 |
| 181 |
* |
| 182 |
* @return array |
| 183 |
*/ |
| 184 |
abstract public function get_input_schema(); |
| 185 |
|
| 186 |
/** |
| 187 |
* JSON Schema for the ability's output. |
| 188 |
* |
| 189 |
* Note that the Abilities API validates the returned value against this, so |
| 190 |
* a schema narrower than what {@see self::execute()} actually returns turns |
| 191 |
* a successful call into a validation error. |
| 192 |
* |
| 193 |
* @since 4.9.0 |
| 194 |
* |
| 195 |
* @return array |
| 196 |
*/ |
| 197 |
abstract public function get_output_schema(); |
| 198 |
|
| 199 |
/** |
| 200 |
* Do the work. |
| 201 |
* |
| 202 |
* @since 4.9.0 |
| 203 |
* |
| 204 |
* @param array $input Validated input. |
| 205 |
* @return array|\WP_Error Plain data on success — never a `{success, data}` envelope. |
| 206 |
*/ |
| 207 |
abstract public function execute( $input ); |
| 208 |
|
| 209 |
/** |
| 210 |
* Developer kill switch for the whole ability surface. |
| 211 |
* |
| 212 |
* @since 4.9.0 |
| 213 |
* |
| 214 |
* @return bool |
| 215 |
*/ |
| 216 |
public static function abilities_enabled() { |
| 217 |
/** |
| 218 |
* Filters whether BetterDocs registers any abilities at all. |
| 219 |
* |
| 220 |
* @since 4.9.0 |
| 221 |
* |
| 222 |
* @param bool $enabled Default true. |
| 223 |
*/ |
| 224 |
return (bool) apply_filters( 'betterdocs_abilities_api_enabled', true ); |
| 225 |
} |
| 226 |
|
| 227 |
/** |
| 228 |
* Whether this particular ability may be registered and executed. |
| 229 |
* |
| 230 |
* @since 4.9.0 |
| 231 |
* |
| 232 |
* @return bool |
| 233 |
*/ |
| 234 |
public function is_enabled() { |
| 235 |
/** |
| 236 |
* Filters whether one ability is enabled. |
| 237 |
* |
| 238 |
* @since 4.9.0 |
| 239 |
* |
| 240 |
* @param bool $enabled Whether the ability surface is on. |
| 241 |
* @param string $id Ability id. |
| 242 |
* @param AbilityBase $ability The ability instance. |
| 243 |
*/ |
| 244 |
return (bool) apply_filters( 'betterdocs_ability_enabled', self::abilities_enabled(), $this->id, $this ); |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Permission callback handed to the Abilities API. |
| 249 |
* |
| 250 |
* Returns a plain bool on purpose. The Abilities API discards a `WP_Error` |
| 251 |
* from this callback and answers with its own generic |
| 252 |
* `ability_invalid_permissions` — and calls `_doing_it_wrong()` on the way, |
| 253 |
* so returning a typed error here would only add log noise. The typed |
| 254 |
* `capability_missing` lives in {@see self::execute_wrapper()}, which is the |
| 255 |
* path the MCP server takes. |
| 256 |
* |
| 257 |
* @since 4.9.0 |
| 258 |
* |
| 259 |
* @return bool |
| 260 |
*/ |
| 261 |
public function permission_callback() { |
| 262 |
if ( ! $this->is_enabled() ) { |
| 263 |
return false; |
| 264 |
} |
| 265 |
|
| 266 |
return current_user_can( $this->capability ); |
| 267 |
} |
| 268 |
|
| 269 |
/** |
| 270 |
* Safety net against an ability that declares no gate at all. |
| 271 |
* |
| 272 |
* This only asserts that *some* capability was named; the capability itself is |
| 273 |
* per-ability (see {@see self::$capability}). |
| 274 |
* |
| 275 |
* @since 4.9.0 |
| 276 |
* |
| 277 |
* @return bool |
| 278 |
*/ |
| 279 |
public function meets_capability_policy() { |
| 280 |
return is_string( $this->capability ) && '' !== $this->capability; |
| 281 |
} |
| 282 |
|
| 283 |
/** |
| 284 |
* Description for the current Pro state. |
| 285 |
* |
| 286 |
* Defaults to the static description. Stubs and the Pro knowledge-base |
| 287 |
* abilities override this to say what is actually true on *this* site — that |
| 288 |
* Pro is missing, inactive, or that a setting is off — so the tool list an |
| 289 |
* agent reads is never misleading. |
| 290 |
* |
| 291 |
* @since 4.9.0 |
| 292 |
* |
| 293 |
* @param array $pro_state Result of `ProState::get()`. |
| 294 |
* @return string |
| 295 |
*/ |
| 296 |
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- the state is the whole point of the override; the default just ignores it. |
| 297 |
public function describe( array $pro_state ) { |
| 298 |
return $this->description; |
| 299 |
} |
| 300 |
|
| 301 |
/** |
| 302 |
* Whether the feature behind this ability needs Pro. |
| 303 |
* |
| 304 |
* @since 4.9.0 |
| 305 |
* |
| 306 |
* @return bool |
| 307 |
*/ |
| 308 |
public function requires_pro() { |
| 309 |
return (bool) $this->requires_pro; |
| 310 |
} |
| 311 |
|
| 312 |
/** |
| 313 |
* MCP-compatible annotations. Subclasses override. |
| 314 |
* |
| 315 |
* Key names are the Abilities API's; `MCPTools` maps them onto the |
| 316 |
* MCP spelling (`readOnlyHint`, `destructiveHint`, `idempotentHint`) when it |
| 317 |
* builds `tools/list`. |
| 318 |
* |
| 319 |
* @since 4.9.0 |
| 320 |
* |
| 321 |
* @return array |
| 322 |
*/ |
| 323 |
public function get_annotations() { |
| 324 |
return [ |
| 325 |
'readonly' => false, |
| 326 |
'destructive' => false, |
| 327 |
'idempotent' => false, |
| 328 |
'priority' => 2.0, |
| 329 |
'openWorldHint' => false |
| 330 |
]; |
| 331 |
} |
| 332 |
|
| 333 |
/** |
| 334 |
* Run the ability with the surrounding contract: capability gate, action |
| 335 |
* hooks, and a `\Throwable` net. |
| 336 |
* |
| 337 |
* Registered as the ability's `execute_callback`, so on the Abilities API |
| 338 |
* path the capability check here is a second opinion the runtime has already |
| 339 |
* formed. It matters on the MCP path, which calls this directly and needs |
| 340 |
* the typed `capability_missing` rather than a bare false. |
| 341 |
* |
| 342 |
* `\Throwable`, not `\Exception`: a `TypeError` from a controller must come |
| 343 |
* back as a typed `upstream_error` naming the real reason, never as a fatal |
| 344 |
* that takes the JSON-RPC response with it. |
| 345 |
* |
| 346 |
* @since 4.9.0 |
| 347 |
* |
| 348 |
* @param array $input Validated input. |
| 349 |
* @return array|\WP_Error |
| 350 |
*/ |
| 351 |
public function execute_wrapper( $input ) { |
| 352 |
if ( ! $this->is_enabled() || ! current_user_can( $this->capability ) ) { |
| 353 |
return AbilityError::capability_missing( $this->capability, $this->permission_phrase() ); |
| 354 |
} |
| 355 |
|
| 356 |
/** |
| 357 |
* Fires before an ability executes. |
| 358 |
* |
| 359 |
* @since 4.9.0 |
| 360 |
* |
| 361 |
* @param string $id Ability id. |
| 362 |
* @param array $input Validated input. |
| 363 |
*/ |
| 364 |
do_action( 'betterdocs_before_ability_execute', $this->id, $input ); |
| 365 |
|
| 366 |
try { |
| 367 |
$output = $this->execute( $input ); |
| 368 |
} catch ( \Throwable $e ) { |
| 369 |
$output = AbilityError::upstream( $e->getMessage(), [ 'ability' => $this->id ] ); |
| 370 |
} |
| 371 |
|
| 372 |
/** |
| 373 |
* Fires after an ability executes, whatever the outcome. |
| 374 |
* |
| 375 |
* @since 4.9.0 |
| 376 |
* |
| 377 |
* @param string $id Ability id. |
| 378 |
* @param array $input Validated input. |
| 379 |
* @param array|\WP_Error $output What the ability returned. |
| 380 |
*/ |
| 381 |
do_action( 'betterdocs_after_ability_execute', $this->id, $input, $output ); |
| 382 |
|
| 383 |
return $output; |
| 384 |
} |
| 385 |
|
| 386 |
/** |
| 387 |
* Call one of BetterDocs' own REST routes in-process and return its data. |
| 388 |
* |
| 389 |
* `rest_do_request()` still runs the route's `permission_callback`, but skips |
| 390 |
* the cookie-nonce check that only applies to real HTTP requests — correct |
| 391 |
* here, because the MCP server has already set the current user to whoever |
| 392 |
* granted the credential. |
| 393 |
* |
| 394 |
* Returns the **unwrapped** payload: an ability answers with plain data, not a |
| 395 |
* `{success, data}` envelope. |
| 396 |
* |
| 397 |
* @since 4.9.0 |
| 398 |
* |
| 399 |
* @param string $method HTTP verb. |
| 400 |
* @param string $route Route beneath the namespace, e.g. `/docs`. |
| 401 |
* @param array $params Query params for GET, body params otherwise. |
| 402 |
* @param string $rest_namespace REST namespace. Defaults to `betterdocs/v1`; the FAQ |
| 403 |
* routes live under the bare `betterdocs` namespace. |
| 404 |
* @return mixed|\WP_Error Response data, or the error the route produced. |
| 405 |
*/ |
| 406 |
protected function dispatch( $method, $route, array $params = [], $rest_namespace = self::NS ) { |
| 407 |
$method = strtoupper( $method ); |
| 408 |
$request = new \WP_REST_Request( $method, '/' . trim( $rest_namespace, '/' ) . $route ); |
| 409 |
|
| 410 |
$request->set_header( 'Content-Type', 'application/json' ); |
| 411 |
|
| 412 |
if ( 'GET' === $method ) { |
| 413 |
$request->set_query_params( $params ); |
| 414 |
} else { |
| 415 |
$request->set_body( wp_json_encode( $params ) ); |
| 416 |
$request->set_body_params( $params ); |
| 417 |
} |
| 418 |
|
| 419 |
$response = rest_do_request( $request ); |
| 420 |
|
| 421 |
if ( $response->is_error() ) { |
| 422 |
return $response->as_error(); |
| 423 |
} |
| 424 |
|
| 425 |
return $response->get_data(); |
| 426 |
} |
| 427 |
|
| 428 |
/** |
| 429 |
* The same call as {@see self::dispatch()}, but handing back the whole |
| 430 |
* response instead of its data. |
| 431 |
* |
| 432 |
* Listing tools need it: `X-WP-Total` and `X-WP-TotalPages` live on the |
| 433 |
* response, and a page that reports the size of the page it returned rather |
| 434 |
* than the number of matches makes a client stop paging early. |
| 435 |
* |
| 436 |
* @since 4.9.0 |
| 437 |
* |
| 438 |
* @param string $method HTTP verb. |
| 439 |
* @param string $route Route beneath the namespace, e.g. `/docs`. |
| 440 |
* @param array $params Query params for GET, body params otherwise. |
| 441 |
* @param string $rest_namespace REST namespace. |
| 442 |
* @return \WP_REST_Response |
| 443 |
*/ |
| 444 |
protected function dispatch_response( $method, $route, array $params = [], $rest_namespace = self::NS ) { |
| 445 |
$method = strtoupper( $method ); |
| 446 |
$request = new \WP_REST_Request( $method, '/' . trim( $rest_namespace, '/' ) . $route ); |
| 447 |
|
| 448 |
$request->set_header( 'Content-Type', 'application/json' ); |
| 449 |
|
| 450 |
if ( 'GET' === $method ) { |
| 451 |
$request->set_query_params( $params ); |
| 452 |
} else { |
| 453 |
$request->set_body( wp_json_encode( $params ) ); |
| 454 |
$request->set_body_params( $params ); |
| 455 |
} |
| 456 |
|
| 457 |
return rest_do_request( $request ); |
| 458 |
} |
| 459 |
|
| 460 |
/** |
| 461 |
* Whether a controller refused a page because it is past the last one. |
| 462 |
* |
| 463 |
* `WP_REST_Posts_Controller::get_items()` answers `rest_post_invalid_page_number` |
| 464 |
* the moment `page` exceeds the available pages and there is at least one |
| 465 |
* match; the terms controller does not (it pages by offset and returns an |
| 466 |
* empty page), so only the post-backed list tools ever meet this. It is not |
| 467 |
* a "not found": the collection exists, the page is merely empty. The terms |
| 468 |
* code is recognised too, for any controller that later adopts the same rule. |
| 469 |
* |
| 470 |
* @since 4.9.0 |
| 471 |
* |
| 472 |
* @param \WP_Error $error The controller error. |
| 473 |
* @return bool |
| 474 |
*/ |
| 475 |
protected function is_page_out_of_range( \WP_Error $error ) { |
| 476 |
return in_array( |
| 477 |
$error->get_error_code(), |
| 478 |
[ 'rest_post_invalid_page_number', 'rest_term_invalid_page_number' ], |
| 479 |
true |
| 480 |
); |
| 481 |
} |
| 482 |
|
| 483 |
/** |
| 484 |
* An empty page carrying the listing's real totals. |
| 485 |
* |
| 486 |
* The out-of-range error carries no counts, so the same query is re-run at |
| 487 |
* page 1 to read `X-WP-Total` / `X-WP-TotalPages` from the headers. The |
| 488 |
* result keeps paging honest: no items, the real `total` and `total_pages`, |
| 489 |
* and the `page` / `per_page` the caller asked for. A page past the end is a |
| 490 |
* normal empty answer, not an error (ADR-059, finding C). |
| 491 |
* |
| 492 |
* @since 4.9.0 |
| 493 |
* |
| 494 |
* @param string $route Route passed to dispatch_response (e.g. '/docs'). |
| 495 |
* @param array $params Query params of the failed request. |
| 496 |
* @param int $page The requested (out-of-range) page. |
| 497 |
* @param int $per_page Requested per_page. |
| 498 |
* @param string $rest_namespace REST namespace. |
| 499 |
* @return array |
| 500 |
*/ |
| 501 |
protected function empty_page( $route, array $params, $page, $per_page, $rest_namespace = self::NS ) { |
| 502 |
$params['page'] = 1; |
| 503 |
$probe = $this->dispatch_response( 'GET', $route, $params, $rest_namespace ); |
| 504 |
$headers = $probe->is_error() ? [] : $probe->get_headers(); |
| 505 |
|
| 506 |
return [ |
| 507 |
'items' => [], |
| 508 |
'total' => isset( $headers['X-WP-Total'] ) ? (int) $headers['X-WP-Total'] : 0, |
| 509 |
'total_pages' => isset( $headers['X-WP-TotalPages'] ) ? (int) $headers['X-WP-TotalPages'] : 1, |
| 510 |
'page' => (int) $page, |
| 511 |
'per_page' => (int) $per_page |
| 512 |
]; |
| 513 |
} |
| 514 |
|
| 515 |
/** |
| 516 |
* Register the ability with the WordPress Abilities API. |
| 517 |
* |
| 518 |
* The `function_exists()` guard is inside the callback, never at hook |
| 519 |
* registration time: which copy of the API owns the global functions is |
| 520 |
* decided by load order, so a copy that lands late must still find us hooked |
| 521 |
* (see `AbilitiesRegistrar`). |
| 522 |
* |
| 523 |
* @since 4.9.0 |
| 524 |
* |
| 525 |
* @return void |
| 526 |
*/ |
| 527 |
public function register() { |
| 528 |
if ( ! function_exists( 'wp_register_ability' ) ) { |
| 529 |
return; |
| 530 |
} |
| 531 |
|
| 532 |
wp_register_ability( |
| 533 |
$this->id, |
| 534 |
[ |
| 535 |
'label' => $this->label, |
| 536 |
'description' => $this->description, |
| 537 |
'category' => $this->category, |
| 538 |
'input_schema' => $this->get_input_schema(), |
| 539 |
'output_schema' => $this->get_output_schema(), |
| 540 |
'permission_callback' => [ $this, 'permission_callback' ], |
| 541 |
'execute_callback' => [ $this, 'execute_wrapper' ], |
| 542 |
'meta' => [ |
| 543 |
'show_in_rest' => true, |
| 544 |
'annotations' => $this->get_annotations(), |
| 545 |
'mcp' => [ |
| 546 |
'public' => false |
| 547 |
], |
| 548 |
'requires_pro' => $this->requires_pro() |
| 549 |
] |
| 550 |
] |
| 551 |
); |
| 552 |
} |
| 553 |
|
| 554 |
/** |
| 555 |
* Ability id. |
| 556 |
* |
| 557 |
* @since 4.9.0 |
| 558 |
* |
| 559 |
* @return string |
| 560 |
*/ |
| 561 |
public function get_id() { |
| 562 |
return $this->id; |
| 563 |
} |
| 564 |
|
| 565 |
/** |
| 566 |
* Human-readable label. |
| 567 |
* |
| 568 |
* @since 4.9.0 |
| 569 |
* |
| 570 |
* @return string |
| 571 |
*/ |
| 572 |
public function get_label() { |
| 573 |
return $this->label; |
| 574 |
} |
| 575 |
|
| 576 |
/** |
| 577 |
* Static description. |
| 578 |
* |
| 579 |
* @since 4.9.0 |
| 580 |
* |
| 581 |
* @return string |
| 582 |
*/ |
| 583 |
public function get_description() { |
| 584 |
return $this->description; |
| 585 |
} |
| 586 |
|
| 587 |
/** |
| 588 |
* Capability this ability is gated on. |
| 589 |
* |
| 590 |
* @since 4.9.0 |
| 591 |
* |
| 592 |
* @return string |
| 593 |
*/ |
| 594 |
public function get_capability() { |
| 595 |
return $this->capability; |
| 596 |
} |
| 597 |
|
| 598 |
/** |
| 599 |
* The phrase `capability_missing` uses to say what was being attempted. |
| 600 |
* Defaults to the label, lowercased; subclasses may override for a better |
| 601 |
* sentence. |
| 602 |
* |
| 603 |
* @since 4.9.0 |
| 604 |
* |
| 605 |
* @return string |
| 606 |
*/ |
| 607 |
protected function permission_phrase() { |
| 608 |
return '' !== $this->label ? lcfirst( $this->label ) : $this->id; |
| 609 |
} |
| 610 |
} |
| 611 |
|