Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
91.95% |
297 / 323 |
|
72.73% |
8 / 11 |
CRAP | |
0.00% |
0 / 1 |
| Boost_Abilities | |
91.95% |
297 / 323 |
|
72.73% |
8 / 11 |
59.75 | |
0.00% |
0 / 1 |
| get_category_slug | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_category_definition | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| get_abilities | |
100.00% |
162 / 162 |
|
100.00% |
1 / 1 |
1 | |||
| can_view_modules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| can_manage_modules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| build_module_index | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| render_module | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| get_modules | |
65.96% |
31 / 47 |
|
0.00% |
0 / 1 |
41.09 | |||
| set_module_status | |
93.22% |
55 / 59 |
|
0.00% |
0 / 1 |
13.05 | |||
| get_speed_score | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
| clear_page_cache | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
3.88 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Jetpack Boost Abilities Registration |
| 4 | * |
| 5 | * Registers Jetpack Boost abilities with the WordPress Abilities API so AI |
| 6 | * agents can read module state, toggle modules, fetch the latest speed score, |
| 7 | * and clear the page cache through the standard `wp-abilities/v1` REST surface. |
| 8 | * |
| 9 | * @package automattic/jetpack-boost |
| 10 | */ |
| 11 | |
| 12 | // @phan-file-suppress PhanUndeclaredFunction, PhanUndeclaredClassMethod @phan-suppress-current-line UnusedSuppression -- Abilities API added in WP 6.9. We guard with function_exists() checks so the class is safe on older WP. @todo Remove this line when the minimum supported WordPress version is 6.9. |
| 13 | |
| 14 | namespace Automattic\Jetpack_Boost\Abilities; |
| 15 | |
| 16 | use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History; |
| 17 | use Automattic\Jetpack\WP_Abilities\Registrar; |
| 18 | use Automattic\Jetpack_Boost\Modules\Features_Index; |
| 19 | use Automattic\Jetpack_Boost\Modules\Module; |
| 20 | use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache; |
| 21 | use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache; |
| 22 | |
| 23 | /** |
| 24 | * Registers Jetpack Boost abilities with the WordPress Abilities API. |
| 25 | * |
| 26 | * Surface (4 abilities, all under the `jetpack-boost/` namespace): |
| 27 | * |
| 28 | * - get-modules — filtered read of every Boost module + submodule. |
| 29 | * - set-module-status — declarative toggle, idempotent. |
| 30 | * - get-speed-score — latest mobile/desktop scores from history. |
| 31 | * - clear-page-cache — flush the Boost page cache for the home URL. |
| 32 | * |
| 33 | * @since $$next-version$$ |
| 34 | */ |
| 35 | class Boost_Abilities extends Registrar { |
| 36 | |
| 37 | /** |
| 38 | * @inheritDoc |
| 39 | */ |
| 40 | public static function get_category_slug(): string { |
| 41 | return 'jetpack-boost'; |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * @inheritDoc |
| 46 | */ |
| 47 | public static function get_category_definition(): array { |
| 48 | return array( |
| 49 | // "Jetpack Boost" is a product name and is not translated. |
| 50 | 'label' => 'Jetpack Boost', |
| 51 | 'description' => __( 'Abilities for inspecting and managing Jetpack Boost performance modules, speed scores, and page cache.', 'jetpack-boost' ), |
| 52 | ); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * @inheritDoc |
| 57 | */ |
| 58 | public static function get_abilities(): array { |
| 59 | $module_object_schema = array( |
| 60 | 'type' => 'object', |
| 61 | 'properties' => array( |
| 62 | 'slug' => array( 'type' => 'string' ), |
| 63 | 'active' => array( 'type' => 'boolean' ), |
| 64 | 'available' => array( 'type' => 'boolean' ), |
| 65 | 'optimizing' => array( 'type' => 'boolean' ), |
| 66 | ), |
| 67 | ); |
| 68 | |
| 69 | return array( |
| 70 | 'jetpack-boost/get-modules' => array( |
| 71 | 'label' => __( 'Get Boost modules', 'jetpack-boost' ), |
| 72 | 'description' => __( 'List Jetpack Boost performance modules and submodules. Returns an array of { slug, active, available, optimizing }. Slugs use underscores (e.g. "critical_css", "page_cache", "image_cdn"). Pass slug to fetch a single module (returns a 0- or 1-element array; unknown slugs yield an empty array, never an error). Pass status to filter by active/inactive/available/optimizing. Use the returned slugs as input to jetpack-boost/set-module-status.', 'jetpack-boost' ), |
| 73 | 'input_schema' => array( |
| 74 | 'type' => 'object', |
| 75 | 'default' => array(), |
| 76 | 'properties' => array( |
| 77 | 'slug' => array( |
| 78 | 'type' => 'string', |
| 79 | 'description' => __( 'Return a single module by its slug (e.g. "critical_css", "page_cache"). Unknown slugs yield an empty array.', 'jetpack-boost' ), |
| 80 | 'minLength' => 1, |
| 81 | ), |
| 82 | 'status' => array( |
| 83 | 'type' => 'string', |
| 84 | 'description' => __( 'Filter by lifecycle state. "active" = enabled by the user. "inactive" = available but disabled. "available" = currently loadable on this site (regardless of enabled state). "optimizing" = active and currently serving optimized output.', 'jetpack-boost' ), |
| 85 | 'enum' => array( 'active', 'inactive', 'available', 'optimizing' ), |
| 86 | ), |
| 87 | 'search' => array( |
| 88 | 'type' => 'string', |
| 89 | 'description' => __( 'Case-insensitive substring match against the module slug.', 'jetpack-boost' ), |
| 90 | 'minLength' => 1, |
| 91 | ), |
| 92 | ), |
| 93 | 'additionalProperties' => false, |
| 94 | ), |
| 95 | 'output_schema' => array( |
| 96 | 'type' => 'array', |
| 97 | 'items' => $module_object_schema, |
| 98 | ), |
| 99 | 'execute_callback' => array( __CLASS__, 'get_modules' ), |
| 100 | 'permission_callback' => array( __CLASS__, 'can_view_modules' ), |
| 101 | 'meta' => array( |
| 102 | 'annotations' => array( |
| 103 | 'readonly' => true, |
| 104 | 'destructive' => false, |
| 105 | 'idempotent' => true, |
| 106 | ), |
| 107 | 'show_in_rest' => true, |
| 108 | 'mcp' => array( |
| 109 | 'public' => true, |
| 110 | 'type' => 'tool', |
| 111 | ), |
| 112 | ), |
| 113 | ), |
| 114 | |
| 115 | 'jetpack-boost/set-module-status' => array( |
| 116 | 'label' => __( 'Set Boost module status', 'jetpack-boost' ), |
| 117 | 'description' => __( 'Enable or disable a single Jetpack Boost module by slug. Required: { slug, active }. Returns { slug, active, changed }. Idempotent: setting a module to its current state returns changed=false. Slugs use underscores (e.g. "critical_css", "page_cache"). Unknown slugs return jetpack_boost_invalid_slug; modules not loadable on this site return jetpack_boost_module_unavailable; always-on modules cannot be disabled and return jetpack_boost_module_always_on — call jetpack-boost/get-modules to enumerate available slugs. Toggling a parent module also drives submodule lifecycle.', 'jetpack-boost' ), |
| 118 | 'input_schema' => array( |
| 119 | 'type' => 'object', |
| 120 | 'required' => array( 'slug', 'active' ), |
| 121 | 'properties' => array( |
| 122 | 'slug' => array( |
| 123 | 'type' => 'string', |
| 124 | 'description' => __( 'Module slug to toggle (e.g. "critical_css", "page_cache").', 'jetpack-boost' ), |
| 125 | 'minLength' => 1, |
| 126 | ), |
| 127 | 'active' => array( |
| 128 | 'type' => 'boolean', |
| 129 | 'description' => __( 'Desired state: true to enable, false to disable.', 'jetpack-boost' ), |
| 130 | ), |
| 131 | ), |
| 132 | 'additionalProperties' => false, |
| 133 | ), |
| 134 | 'output_schema' => array( |
| 135 | 'type' => 'object', |
| 136 | 'properties' => array( |
| 137 | 'slug' => array( 'type' => 'string' ), |
| 138 | 'active' => array( 'type' => 'boolean' ), |
| 139 | 'changed' => array( 'type' => 'boolean' ), |
| 140 | ), |
| 141 | ), |
| 142 | 'execute_callback' => array( __CLASS__, 'set_module_status' ), |
| 143 | 'permission_callback' => array( __CLASS__, 'can_manage_modules' ), |
| 144 | 'meta' => array( |
| 145 | 'annotations' => array( |
| 146 | 'readonly' => false, |
| 147 | 'destructive' => false, |
| 148 | 'idempotent' => true, |
| 149 | ), |
| 150 | 'show_in_rest' => true, |
| 151 | 'mcp' => array( |
| 152 | 'public' => true, |
| 153 | 'type' => 'tool', |
| 154 | ), |
| 155 | ), |
| 156 | ), |
| 157 | |
| 158 | 'jetpack-boost/get-speed-score' => array( |
| 159 | 'label' => __( 'Get latest speed score', 'jetpack-boost' ), |
| 160 | 'description' => __( 'Return the most recent Jetpack Boost speed score for the home URL. Returns { mobile, desktop, timestamp, is_stale, has_history }. mobile/desktop are integers 0-100 (Google PageSpeed scale) or null when no score has been recorded. timestamp is a Unix epoch in seconds. is_stale=true means the latest score is older than 24 hours or invalidated by a site change; agents should request a refresh from the Boost UI before quoting it. has_history=false means no scores have been recorded yet.', 'jetpack-boost' ), |
| 161 | 'input_schema' => array( |
| 162 | 'type' => 'object', |
| 163 | 'default' => array(), |
| 164 | 'properties' => array(), |
| 165 | 'additionalProperties' => false, |
| 166 | ), |
| 167 | 'output_schema' => array( |
| 168 | 'type' => 'object', |
| 169 | 'properties' => array( |
| 170 | 'mobile' => array( 'type' => array( 'integer', 'null' ) ), |
| 171 | 'desktop' => array( 'type' => array( 'integer', 'null' ) ), |
| 172 | 'timestamp' => array( 'type' => array( 'integer', 'null' ) ), |
| 173 | 'is_stale' => array( 'type' => 'boolean' ), |
| 174 | 'has_history' => array( 'type' => 'boolean' ), |
| 175 | ), |
| 176 | ), |
| 177 | 'execute_callback' => array( __CLASS__, 'get_speed_score' ), |
| 178 | 'permission_callback' => array( __CLASS__, 'can_view_modules' ), |
| 179 | 'meta' => array( |
| 180 | 'annotations' => array( |
| 181 | 'readonly' => true, |
| 182 | 'destructive' => false, |
| 183 | 'idempotent' => true, |
| 184 | ), |
| 185 | 'show_in_rest' => true, |
| 186 | 'mcp' => array( |
| 187 | 'public' => true, |
| 188 | 'type' => 'tool', |
| 189 | ), |
| 190 | ), |
| 191 | ), |
| 192 | |
| 193 | 'jetpack-boost/clear-page-cache' => array( |
| 194 | 'label' => __( 'Clear page cache', 'jetpack-boost' ), |
| 195 | 'description' => __( 'Clear every cached page under the site home URL. Idempotent: re-running on an empty cache is a no-op. Returns { cleared, message }. cleared=true means the clear request was dispatched against an active page_cache module; it does not promise that cached files actually existed (the underlying API does not surface a count). Requires the page_cache module to be active; if it is not, returns jetpack_boost_page_cache_inactive — enable it first via jetpack-boost/set-module-status with slug="page_cache".', 'jetpack-boost' ), |
| 196 | 'input_schema' => array( |
| 197 | 'type' => 'object', |
| 198 | 'default' => array(), |
| 199 | 'properties' => array(), |
| 200 | 'additionalProperties' => false, |
| 201 | ), |
| 202 | 'output_schema' => array( |
| 203 | 'type' => 'object', |
| 204 | 'properties' => array( |
| 205 | 'cleared' => array( 'type' => 'boolean' ), |
| 206 | 'message' => array( 'type' => 'string' ), |
| 207 | ), |
| 208 | ), |
| 209 | 'execute_callback' => array( __CLASS__, 'clear_page_cache' ), |
| 210 | 'permission_callback' => array( __CLASS__, 'can_manage_modules' ), |
| 211 | 'meta' => array( |
| 212 | 'annotations' => array( |
| 213 | 'readonly' => false, |
| 214 | 'destructive' => false, |
| 215 | 'idempotent' => true, |
| 216 | ), |
| 217 | 'show_in_rest' => true, |
| 218 | 'mcp' => array( |
| 219 | 'public' => true, |
| 220 | 'type' => 'tool', |
| 221 | ), |
| 222 | ), |
| 223 | ), |
| 224 | ); |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Permission check for read-only abilities. |
| 229 | * |
| 230 | * Boost has no domain-specific capability; every existing Boost surface |
| 231 | * gates on `manage_options`. We mirror that here so abilities are no more |
| 232 | * (or less) permissive than the REST and admin surfaces. |
| 233 | * |
| 234 | * @since $$next-version$$ |
| 235 | */ |
| 236 | public static function can_view_modules(): bool { |
| 237 | return is_user_logged_in() && current_user_can( 'manage_options' ); |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Permission check for write abilities. |
| 242 | * |
| 243 | * @since $$next-version$$ |
| 244 | */ |
| 245 | public static function can_manage_modules(): bool { |
| 246 | return is_user_logged_in() && current_user_can( 'manage_options' ); |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Cache for `build_module_index()`. Module construction touches options for |
| 251 | * every feature, so we memoise per-request — both `get_modules()` and |
| 252 | * `set_module_status()` consume the same map. |
| 253 | * |
| 254 | * @var array<string, Module>|null |
| 255 | */ |
| 256 | private static $module_index_cache = null; |
| 257 | |
| 258 | /** |
| 259 | * Build a `Module` instance for every feature + submodule, keyed by slug. |
| 260 | * |
| 261 | * @return array<string, Module> |
| 262 | */ |
| 263 | private static function build_module_index(): array { |
| 264 | if ( null !== self::$module_index_cache ) { |
| 265 | return self::$module_index_cache; |
| 266 | } |
| 267 | |
| 268 | $index = array(); |
| 269 | foreach ( Features_Index::get_all_features() as $feature_class ) { |
| 270 | $module = new Module( new $feature_class() ); |
| 271 | $index[ $module->get_slug() ] = $module; |
| 272 | } |
| 273 | |
| 274 | self::$module_index_cache = $index; |
| 275 | return $index; |
| 276 | } |
| 277 | |
| 278 | private static function render_module( Module $module ): array { |
| 279 | return array( |
| 280 | 'slug' => $module->get_slug(), |
| 281 | 'active' => $module->is_available() && $module->is_enabled(), |
| 282 | 'available' => $module->is_available(), |
| 283 | 'optimizing' => $module->is_optimizing(), |
| 284 | ); |
| 285 | } |
| 286 | |
| 287 | /** |
| 288 | * Execute: filtered read of Boost modules. |
| 289 | * |
| 290 | * @since $$next-version$$ |
| 291 | * |
| 292 | * @param array|null $input Input matching the ability's input_schema. |
| 293 | * @return array|\WP_Error |
| 294 | */ |
| 295 | public static function get_modules( $input = null ) { |
| 296 | $input = is_array( $input ) ? $input : array(); |
| 297 | $index = self::build_module_index(); |
| 298 | |
| 299 | // Single-slug short-circuit — return 0- or 1-element array, same shape as the list case. |
| 300 | // A non-string slug is invalid input (not "unknown"); rejecting it prevents the bad-shape |
| 301 | // fall-through where slug:123 would silently return every module. |
| 302 | if ( isset( $input['slug'] ) ) { |
| 303 | if ( ! is_string( $input['slug'] ) ) { |
| 304 | return new \WP_Error( |
| 305 | 'jetpack_boost_invalid_slug', |
| 306 | __( 'The slug parameter must be a string.', 'jetpack-boost' ) |
| 307 | ); |
| 308 | } |
| 309 | if ( '' !== $input['slug'] ) { |
| 310 | return isset( $index[ $input['slug'] ] ) |
| 311 | ? array( self::render_module( $index[ $input['slug'] ] ) ) |
| 312 | : array(); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | $status_filter = isset( $input['status'] ) && is_string( $input['status'] ) && '' !== $input['status'] |
| 317 | ? $input['status'] |
| 318 | : null; |
| 319 | $search_filter = isset( $input['search'] ) && is_string( $input['search'] ) && '' !== $input['search'] |
| 320 | ? $input['search'] |
| 321 | : null; |
| 322 | |
| 323 | $out = array(); |
| 324 | foreach ( $index as $slug => $module ) { |
| 325 | $rendered = self::render_module( $module ); |
| 326 | |
| 327 | if ( null !== $status_filter ) { |
| 328 | $matches = false; |
| 329 | switch ( $status_filter ) { |
| 330 | case 'active': |
| 331 | $matches = $rendered['active']; |
| 332 | break; |
| 333 | case 'inactive': |
| 334 | $matches = $rendered['available'] && ! $rendered['active']; |
| 335 | break; |
| 336 | case 'available': |
| 337 | $matches = $rendered['available']; |
| 338 | break; |
| 339 | case 'optimizing': |
| 340 | $matches = $rendered['optimizing']; |
| 341 | break; |
| 342 | } |
| 343 | if ( ! $matches ) { |
| 344 | continue; |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | // Boost slugs are ASCII snake_case; stripos is sufficient and avoids ext-mbstring. |
| 349 | if ( null !== $search_filter && false === stripos( $slug, $search_filter ) ) { |
| 350 | continue; |
| 351 | } |
| 352 | |
| 353 | $out[] = $rendered; |
| 354 | } |
| 355 | |
| 356 | // Deterministic ordering — agents diff results across calls. |
| 357 | usort( |
| 358 | $out, |
| 359 | static function ( $a, $b ) { |
| 360 | return strcmp( $a['slug'], $b['slug'] ); |
| 361 | } |
| 362 | ); |
| 363 | |
| 364 | return $out; |
| 365 | } |
| 366 | |
| 367 | /** |
| 368 | * Execute: declarative module toggle. Idempotent. |
| 369 | * |
| 370 | * @since $$next-version$$ |
| 371 | * |
| 372 | * @param array|null $input Input matching the ability's input_schema. |
| 373 | * @return array|\WP_Error |
| 374 | */ |
| 375 | public static function set_module_status( $input = null ) { |
| 376 | $input = is_array( $input ) ? $input : array(); |
| 377 | |
| 378 | // Required-id validation: not empty(), so "0" remains a legal slug if a future module ever uses it. |
| 379 | if ( ! isset( $input['slug'] ) || ! is_string( $input['slug'] ) || '' === $input['slug'] ) { |
| 380 | return new \WP_Error( |
| 381 | 'jetpack_boost_missing_slug', |
| 382 | __( 'A module slug is required. Call jetpack-boost/get-modules to enumerate available slugs.', 'jetpack-boost' ) |
| 383 | ); |
| 384 | } |
| 385 | if ( ! array_key_exists( 'active', $input ) ) { |
| 386 | return new \WP_Error( |
| 387 | 'jetpack_boost_missing_active', |
| 388 | __( 'A desired active state (boolean) is required.', 'jetpack-boost' ) |
| 389 | ); |
| 390 | } |
| 391 | if ( ! is_bool( $input['active'] ) ) { |
| 392 | return new \WP_Error( |
| 393 | 'jetpack_boost_invalid_active', |
| 394 | __( 'The active parameter must be a boolean. Strings like "true" / "false" are not accepted.', 'jetpack-boost' ) |
| 395 | ); |
| 396 | } |
| 397 | |
| 398 | $slug = $input['slug']; |
| 399 | $desired = $input['active']; |
| 400 | $index = self::build_module_index(); |
| 401 | |
| 402 | if ( ! isset( $index[ $slug ] ) ) { |
| 403 | return new \WP_Error( |
| 404 | 'jetpack_boost_invalid_slug', |
| 405 | __( 'Unknown Boost module slug. Call jetpack-boost/get-modules to enumerate available slugs.', 'jetpack-boost' ) |
| 406 | ); |
| 407 | } |
| 408 | |
| 409 | $module = $index[ $slug ]; |
| 410 | |
| 411 | if ( ! $module->is_available() ) { |
| 412 | return new \WP_Error( |
| 413 | 'jetpack_boost_module_unavailable', |
| 414 | __( 'This module is not available on this site (e.g. requires a connection or a paid plan).', 'jetpack-boost' ) |
| 415 | ); |
| 416 | } |
| 417 | |
| 418 | // Always-on modules ignore the persisted option at runtime. Writing here would leave |
| 419 | // on-disk state diverged from runtime state with no rollback, so refuse the write up front. |
| 420 | if ( $module->is_always_on() ) { |
| 421 | if ( $desired ) { |
| 422 | return array( |
| 423 | 'slug' => $slug, |
| 424 | 'active' => true, |
| 425 | 'changed' => false, |
| 426 | ); |
| 427 | } |
| 428 | return new \WP_Error( |
| 429 | 'jetpack_boost_module_always_on', |
| 430 | __( 'This module is always on and cannot be disabled.', 'jetpack-boost' ) |
| 431 | ); |
| 432 | } |
| 433 | |
| 434 | $current = $module->is_enabled(); |
| 435 | if ( $desired === $current ) { |
| 436 | return array( |
| 437 | 'slug' => $slug, |
| 438 | 'active' => $current, |
| 439 | 'changed' => false, |
| 440 | ); |
| 441 | } |
| 442 | |
| 443 | if ( ! $module->update( $desired ) ) { |
| 444 | return new \WP_Error( |
| 445 | 'jetpack_boost_module_update_failed', |
| 446 | __( 'Failed to persist the module status.', 'jetpack-boost' ) |
| 447 | ); |
| 448 | } |
| 449 | |
| 450 | /** |
| 451 | * Fires when a module is enabled or disabled through the abilities surface. |
| 452 | * |
| 453 | * Mirrors the action emitted by `Modules_State_Entry::set()` so submodule |
| 454 | * lifecycle handlers fire identically regardless of caller. |
| 455 | * |
| 456 | * @param string $module_slug The module slug. |
| 457 | * @param bool $is_active The new state. |
| 458 | */ |
| 459 | do_action( 'jetpack_boost_module_status_updated', $slug, $desired ); |
| 460 | |
| 461 | return array( |
| 462 | 'slug' => $slug, |
| 463 | 'active' => $desired, |
| 464 | 'changed' => true, |
| 465 | ); |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Execute: latest speed score for the home URL. |
| 470 | * |
| 471 | * @since $$next-version$$ |
| 472 | * |
| 473 | * @param array|null $input Unused; ability has no inputs. |
| 474 | * @return array |
| 475 | */ |
| 476 | public static function get_speed_score( $input = null ) { |
| 477 | unset( $input ); |
| 478 | |
| 479 | $history = new Speed_Score_History( home_url() ); |
| 480 | $latest = $history->latest(); |
| 481 | |
| 482 | if ( null === $latest ) { |
| 483 | return array( |
| 484 | 'mobile' => null, |
| 485 | 'desktop' => null, |
| 486 | 'timestamp' => null, |
| 487 | 'is_stale' => false, |
| 488 | 'has_history' => false, |
| 489 | ); |
| 490 | } |
| 491 | |
| 492 | // `scores` may be stored as either an associative array or stdClass depending on |
| 493 | // the API response payload. Normalise to array before reading. |
| 494 | $scores = isset( $latest['scores'] ) ? (array) $latest['scores'] : array(); |
| 495 | $mobile = isset( $scores['mobile'] ) && is_numeric( $scores['mobile'] ) ? (int) $scores['mobile'] : null; |
| 496 | $desktop = isset( $scores['desktop'] ) && is_numeric( $scores['desktop'] ) ? (int) $scores['desktop'] : null; |
| 497 | |
| 498 | return array( |
| 499 | 'mobile' => $mobile, |
| 500 | 'desktop' => $desktop, |
| 501 | 'timestamp' => isset( $latest['timestamp'] ) ? (int) $latest['timestamp'] : null, |
| 502 | 'is_stale' => $history->is_stale(), |
| 503 | 'has_history' => true, |
| 504 | ); |
| 505 | } |
| 506 | |
| 507 | /** |
| 508 | * Execute: clear the Boost page cache for the home URL. |
| 509 | * |
| 510 | * @since $$next-version$$ |
| 511 | * |
| 512 | * @param array|null $input Unused; ability has no inputs. |
| 513 | * @return array|\WP_Error |
| 514 | */ |
| 515 | public static function clear_page_cache( $input = null ) { |
| 516 | unset( $input ); |
| 517 | |
| 518 | $page_cache = new Module( new Page_Cache() ); |
| 519 | if ( ! $page_cache->is_available() || ! $page_cache->is_enabled() ) { |
| 520 | return new \WP_Error( |
| 521 | 'jetpack_boost_page_cache_inactive', |
| 522 | __( 'The page_cache module is not active. Enable it first via jetpack-boost/set-module-status with slug="page_cache" and active=true.', 'jetpack-boost' ) |
| 523 | ); |
| 524 | } |
| 525 | |
| 526 | $cache = new Boost_Cache(); |
| 527 | // Boost_Cache::delete_recursive() returns void — no useful success/no-op signal |
| 528 | // to surface to the agent. Reporting `cleared: true` after a successful module-active |
| 529 | // gate is the most honest answer we can give. |
| 530 | $cache->delete_recursive( home_url() ); |
| 531 | |
| 532 | return array( |
| 533 | 'cleared' => true, |
| 534 | 'message' => __( 'Page cache cleared.', 'jetpack-boost' ), |
| 535 | ); |
| 536 | } |
| 537 | } |