| @@ -29,8 +29,9 @@ | ||
| 29 | 29 | use XSpeed\Server; |
| 30 | 30 | use XSpeed\Admin; |
| 31 | 31 | use XSpeed\Settings; |
| 32 | 32 | use XSpeed\Settings_Manager; |
| 33 | +use XSpeed\Module_Registry; | |
| 33 | 34 | use XSpeed\Pro_Audit; |
| 34 | 35 | use XSpeed\Cache_Benchmark; |
| 35 | 36 | use XSpeed\Tier_Registry; |
| 36 | 37 | use XSpeed\Database_Cleaner; |
| @@ -39,9 +40,9 @@ | ||
| 39 | 40 | |
| 40 | 41 | final class Mcp_Tools { |
| 41 | 42 | |
| 42 | 43 | /** Valid cache purge types. */ |
| 43 | - public const PURGE_TYPES = array( 'all', 'page', 'assets', 'object', 'rest' ); | |
| 44 | + public const PURGE_TYPES = array( 'all', 'page', 'assets', 'object', 'rest', 'cloudflare', 'cdn' ); | |
| 44 | 45 | |
| 45 | 46 | /** |
| 46 | 47 | * Per-call read-only override. Null means "defer to the pairing token's |
| 47 | 48 | * scope" (the JSON-RPC path that predates OAuth). true/false is set by |
| @@ -144,9 +145,9 @@ | ||
| 144 | 145 | 'write' => false, |
| 145 | 146 | 'handler' => array( self::class, 'get_site_info' ), |
| 146 | 147 | ), |
| 147 | 148 | 'optimize_site' => array( |
| 148 | - 'description' => 'Make this site faster, end to end: measure, apply the recommended settings ONE AT A TIME, check the page still renders after each, and undo any change that breaks it. Returns what was applied, the site\'s last recorded score, `next_steps` (riskier settings that could help but are NOT applied automatically), and `unfixable` (problems no caching plugin can reach). ALWAYS relay all three to the user: report the score and what is still wrong, then — if `next_steps` is non-empty — describe each one WITH its stated `risk` and ASK whether to run again with aggressiveness "aggressive". Never enable aggressive settings without the user agreeing first, and never present `unfixable` items as things you can solve; they need the site owner or the host. A site where nothing was left to do is a real, good answer — say so plainly rather than apologising or retrying. Use `dry_run` to preview the plan.', | |
| 149 | + 'description' => 'Make this site faster, end to end: measure, apply the recommended settings ONE AT A TIME, check the page still renders after each, and undo any change that breaks it. Returns what was applied, the site\'s performance score, `next_steps` (riskier settings that could help but are NOT applied automatically), and `unfixable` (problems no caching plugin can reach). ALWAYS relay all three to the user: report the score and what is still wrong, then — if `next_steps` is non-empty — describe each one WITH its stated `risk` and ASK whether to run again with aggressiveness "aggressive". Never enable aggressive settings without the user agreeing first, and never present `unfixable` items as things you can solve; they need the site owner or the host. A site where nothing was left to do is a real, good answer — say so plainly rather than apologising or retrying. Use `dry_run` to preview the plan. The `score` object carries `age_seconds` and `stale`: quote the score WITH how recently it was measured, and never describe a stale score as the result this run produced — when a measurement could not be taken, say the number is old rather than implying it is current. WHEN CHANGES WERE APPLIED the response carries `verify_urls` and `verify_note`: the safety checks read HTML in PHP and cannot execute JavaScript, so a page can pass every one of them and still be broken in a browser. Before reporting success, OPEN each URL in `verify_urls` if you have any way to load a page and confirm it renders with no console errors; if you cannot, tell the user those URLs need checking and what a problem would look like. `verified: true` means the HTML checks passed — it is not a statement that the site works.', | |
| 149 | 150 | 'inputSchema' => self::object_schema( |
| 150 | 151 | array( |
| 151 | 152 | 'aggressiveness' => array( |
| 152 | 153 | 'type' => 'string', |
| @@ -156,8 +157,24 @@ | ||
| 156 | 157 | 'dry_run' => array( |
| 157 | 158 | 'type' => 'boolean', |
| 158 | 159 | 'description' => 'Return the plan without changing anything.', |
| 159 | 160 | ), |
| 161 | + 'measure_score' => array( | |
| 162 | + 'type' => 'string', | |
| 163 | + 'enum' => array( 'auto', 'never', 'always' ), | |
| 164 | + 'description' => 'Whether to take a fresh PageSpeed measurement. auto (default) measures when the stored score is stale and after changes land; never reuses the stored score; always measures even for a dry run. Measurements are rate-limited, so a run inside the cooldown returns the stored score with its age rather than a new one.', | |
| 165 | + ), | |
| 166 | + 'target_score' => array( | |
| 167 | + 'type' => 'integer', | |
| 168 | + 'minimum' => 1, | |
| 169 | + 'maximum' => 100, | |
| 170 | + 'description' => 'Repeat the optimize cycle toward this score instead of running a single pass. Each round costs a real PageSpeed measurement and up to two minutes, so pass it only when the user asked for a specific number. Requires the iterative tuner; without it the run is a single pass and the report says so in `stopped_because`. The run also stops early when further rounds stop helping — either way, read `stopped_because` and relay it rather than retrying.', | |
| 171 | + ), | |
| 172 | + 'max_rounds' => array( | |
| 173 | + 'type' => 'integer', | |
| 174 | + 'minimum' => 1, | |
| 175 | + 'description' => 'Ceiling on rounds when target_score is set. Clamped to what the tuner allows.', | |
| 176 | + ), | |
| 160 | 177 | ), |
| 161 | 178 | array() |
| 162 | 179 | ), |
| 163 | 180 | 'write' => true, |
| @@ -175,9 +192,9 @@ | ||
| 175 | 192 | 'write' => false, |
| 176 | 193 | 'handler' => array( self::class, 'get_pro_audit' ), |
| 177 | 194 | ), |
| 178 | 195 | 'purge_cache' => array( |
| 179 | - 'description' => 'Purge the site cache. "type" selects what to purge: all, page, assets, object, or rest. Defaults to all.', | |
| 196 | + 'description' => 'Purge the site cache and report what was actually cleared, what was skipped and why. "type" selects what to purge: all, page, assets, object, rest, cloudflare, or cdn. Defaults to all.', | |
| 180 | 197 | 'inputSchema' => self::object_schema( |
| 181 | 198 | array( |
| 182 | 199 | 'type' => array( |
| 183 | 200 | 'type' => 'string', |
| @@ -279,9 +296,9 @@ | ||
| 279 | 296 | 'write' => true, |
| 280 | 297 | 'handler' => array( self::class, 'start_preloader' ), |
| 281 | 298 | ), |
| 282 | 299 | 'run_score' => array( |
| 283 | - 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) against this site and return the score plus Core Web Vitals. Available on every install. Spends the site\'s own configured API quota. Use get_score_history to read past runs without starting a new one.', | |
| 300 | + 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) against this site. Running it counts as the opt-in for external scores. With the site\'s own API key the score plus Core Web Vitals come back in the response; a keyless PageSpeed audit on a Hub-connected site is queued instead — the response says so, and the result lands in the history a minute or two later (poll with run_command "score status", or read get_score_history). Spends the site\'s own configured API quota when a key is set.', | |
| 284 | 301 | 'inputSchema' => self::object_schema( |
| 285 | 302 | array( |
| 286 | 303 | 'target' => array( |
| 287 | 304 | 'type' => 'string', |
| @@ -317,9 +334,9 @@ | ||
| 317 | 334 | 'write' => true, |
| 318 | 335 | 'handler' => array( self::class, 'run_score' ), |
| 319 | 336 | ), |
| 320 | 337 | 'run_pagespeed' => array( |
| 321 | - 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) and return the score + Core Web Vitals. Defaults to the site home page, mobile strategy. Requires external scores to be enabled in settings — the plugin makes no outbound calls otherwise.', | |
| 338 | + 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) and return the score + Core Web Vitals. Defaults to the site home page, mobile strategy. Running it counts as the opt-in for external scores; a keyless PageSpeed audit routes through xSpeed Hub when the site is connected.', | |
| 322 | 339 | 'inputSchema' => self::object_schema( |
| 323 | 340 | array( |
| 324 | 341 | 'url' => array( |
| 325 | 342 | 'type' => 'string', |
| @@ -619,8 +636,9 @@ | ||
| 619 | 636 | 'update_settings' => 'xspeed settings', |
| 620 | 637 | 'run_pagespeed' => 'xspeed psi', |
| 621 | 638 | 'get_health' => 'xspeed health', |
| 622 | 639 | 'get_score_history' => 'xspeed score', |
| 640 | + 'purge_cache' => 'xspeed purge', | |
| 623 | 641 | ); |
| 624 | 642 | |
| 625 | 643 | $commands = Cli_Bridge::commands(); |
| 626 | 644 | foreach ( $conditional as $tool => $command ) { |
| @@ -1157,14 +1175,29 @@ | ||
| 1157 | 1175 | * @param array<string,mixed> $args Tool arguments. |
| 1158 | 1176 | * @return array<string,mixed>|\WP_Error |
| 1159 | 1177 | */ |
| 1160 | 1178 | public static function optimize_site( array $args = array() ) { |
| 1161 | - return \XSpeed\Optimize_Runner::run( | |
| 1162 | - array( | |
| 1163 | - 'aggressiveness' => (string) ( $args['aggressiveness'] ?? 'standard' ), | |
| 1164 | - 'dry_run' => (bool) ( $args['dry_run'] ?? false ), | |
| 1165 | - ) | |
| 1179 | + $run = array( | |
| 1180 | + 'aggressiveness' => (string) ( $args['aggressiveness'] ?? 'standard' ), | |
| 1181 | + 'dry_run' => (bool) ( $args['dry_run'] ?? false ), | |
| 1182 | + 'measure_score' => (string) ( $args['measure_score'] ?? 'auto' ), | |
| 1166 | 1183 | ); |
| 1184 | + | |
| 1185 | + // Tuning arguments are forwarded ONLY when present, and are not | |
| 1186 | + // understood by Free — a listener on `xspeed_optimize_report` reads | |
| 1187 | + // them from that filter's `$context`. Passing them through rather | |
| 1188 | + // than naming them in the array above is deliberate: this handler | |
| 1189 | + // builds an explicit whitelist, so an argument it does not list is | |
| 1190 | + // silently dropped. A caller asking to reach a score would have got a | |
| 1191 | + // single pass and a success response — wrong behaviour with no error, | |
| 1192 | + // which is the expensive kind to diagnose. | |
| 1193 | + foreach ( array( 'target_score', 'max_rounds' ) as $key ) { | |
| 1194 | + if ( isset( $args[ $key ] ) && is_numeric( $args[ $key ] ) ) { | |
| 1195 | + $run[ $key ] = (int) $args[ $key ]; | |
| 1196 | + } | |
| 1197 | + } | |
| 1198 | + | |
| 1199 | + return \XSpeed\Optimize_Runner::run( $run ); | |
| 1167 | 1200 | } |
| 1168 | 1201 | |
| 1169 | 1202 | /** |
| 1170 | 1203 | * Before/after cache benchmark timings. |
| @@ -1212,12 +1245,43 @@ | ||
| 1212 | 1245 | } |
| 1213 | 1246 | // Named source, not the default "manual": the purge log's whole job |
| 1214 | 1247 | // is to let an admin see that the cache cleared because an assistant |
| 1215 | 1248 | // asked, not because someone clicked. |
| 1216 | - $count = Cache::purge_type( $type, __( 'AI assistant', 'xspeed' ) ); | |
| 1249 | + $cause = __( 'AI assistant', 'xspeed' ); | |
| 1250 | + | |
| 1251 | + /* | |
| 1252 | + * `page`, `assets` and `rest` are fine-grained slices of the local | |
| 1253 | + * sweep with no target of their own, and they predate this tool's | |
| 1254 | + * per-store report — an assistant asking for `page` means the HTML, | |
| 1255 | + * not the HTML plus the minified bundles plus every purge listener. | |
| 1256 | + * They stay on purge_type() so their meaning does not change under | |
| 1257 | + * callers already relying on it. | |
| 1258 | + * | |
| 1259 | + * Everything else routes through the runner — the same core function | |
| 1260 | + * the CLI and the REST callback use — so an assistant told "cache | |
| 1261 | + * cleared" is reading the same per-store verdict a human would get, | |
| 1262 | + * including a Cloudflare zone that refused the purge. | |
| 1263 | + */ | |
| 1264 | + if ( in_array( $type, array( 'page', 'assets', 'rest' ), true ) ) { | |
| 1265 | + return array( | |
| 1266 | + 'purged' => $type, | |
| 1267 | + 'count' => Cache::purge_type( $type, $cause ), | |
| 1268 | + 'ok' => true, | |
| 1269 | + 'stats' => Cache::get_stats(), | |
| 1270 | + ); | |
| 1271 | + } | |
| 1272 | + | |
| 1273 | + $report = \XSpeed\Purge_Runner::run( array( $type ), $cause ); | |
| 1274 | + $count = 0; | |
| 1275 | + foreach ( $report['types'] as $row ) { | |
| 1276 | + $count += (int) $row['entries']; | |
| 1277 | + } | |
| 1278 | + | |
| 1217 | 1279 | return array( |
| 1218 | 1280 | 'purged' => $type, |
| 1219 | 1281 | 'count' => $count, |
| 1282 | + 'ok' => $report['ok'], | |
| 1283 | + 'report' => $report['types'], | |
| 1220 | 1284 | 'stats' => Cache::get_stats(), |
| 1221 | 1285 | ); |
| 1222 | 1286 | } |
| 1223 | 1287 | |
| @@ -1497,13 +1561,26 @@ | ||
| 1497 | 1561 | * @return true|\WP_Error True when every key would be applied. |
| 1498 | 1562 | */ |
| 1499 | 1563 | private static function inspect_or_error( string $module, array $values ) { |
| 1500 | 1564 | $report = Settings_Manager::inspect_input( $module, $values ); |
| 1501 | - if ( empty( $report['unknown'] ) && empty( $report['invalid'] ) ) { | |
| 1565 | + if ( empty( $report['unknown'] ) && empty( $report['invalid'] ) && empty( $report['locked'] ) ) { | |
| 1502 | 1566 | return true; |
| 1503 | 1567 | } |
| 1504 | 1568 | |
| 1505 | 1569 | $parts = array(); |
| 1570 | + // A field pinned by a wp-config.php constant cannot be written. Say so | |
| 1571 | + // rather than returning a success the agent relays as "changed" over a | |
| 1572 | + // write that update() would silently drop. (#398) | |
| 1573 | + foreach ( $report['locked'] as $key ) { | |
| 1574 | + $spec = ( Module_Registry::get( $module ) ? Module_Registry::get( $module )->settings_schema()[ $key ] ?? array() : array() ); | |
| 1575 | + $constant = Settings_Manager::effective_constant( $module, $key, is_array( $spec ) ? $spec : array() ); | |
| 1576 | + $parts[] = sprintf( | |
| 1577 | + /* translators: 1: setting key, 2: wp-config.php constant name. */ | |
| 1578 | + __( '"%1$s" is defined in wp-config.php as %2$s and cannot be changed here', 'xspeed' ), | |
| 1579 | + $key, | |
| 1580 | + (string) $constant | |
| 1581 | + ); | |
| 1582 | + } | |
| 1506 | 1583 | foreach ( $report['unknown'] as $key ) { |
| 1507 | 1584 | $detail = sprintf( |
| 1508 | 1585 | /* translators: 1: setting key, 2: module slug. */ |
| 1509 | 1586 | __( '"%1$s" is not a setting of module "%2$s"', 'xspeed' ), |
| @@ -1544,8 +1621,9 @@ | ||
| 1544 | 1621 | array( |
| 1545 | 1622 | 'status' => 400, |
| 1546 | 1623 | 'refused_unknown' => $report['unknown'], |
| 1547 | 1624 | 'refused_invalid' => $report['invalid'], |
| 1625 | + 'refused_locked' => $report['locked'], | |
| 1548 | 1626 | 'would_apply' => $report['applied'], |
| 1549 | 1627 | ) |
| 1550 | 1628 | ); |
| 1551 | 1629 | } |