| @@ -19,15 +19,22 @@ | ||
| 19 | 19 | |
| 20 | 20 | use ThinkRank\SEO\Site_Identity_Manager; |
| 21 | 21 | use ThinkRank\AI\Manager as AI_Manager; |
| 22 | 22 | use ThinkRank\API\Traits\CSRF_Protection; |
| 23 | +use ThinkRank\API\Traits\Context_Authorization; | |
| 23 | 24 | use WP_REST_Controller; |
| 24 | 25 | use WP_REST_Request; |
| 25 | 26 | use WP_REST_Response; |
| 26 | 27 | use WP_Error; |
| 27 | 28 | |
| 29 | +// Prevent direct access | |
| 30 | +if (!defined('ABSPATH')) { | |
| 31 | + exit; | |
| 32 | +} | |
| 33 | + | |
| 28 | 34 | // Load CSRF Protection trait |
| 29 | 35 | require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php'; |
| 36 | +require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-context-authorization.php'; | |
| 30 | 37 | |
| 31 | 38 | /** |
| 32 | 39 | * Site Identity API Endpoints Class |
| 33 | 40 | * |
| @@ -39,8 +46,9 @@ | ||
| 39 | 46 | * @since 1.0.0 |
| 40 | 47 | */ |
| 41 | 48 | class Site_Identity_Endpoint extends WP_REST_Controller { |
| 42 | 49 | use CSRF_Protection; |
| 50 | + use Context_Authorization; | |
| 43 | 51 | |
| 44 | 52 | /** |
| 45 | 53 | * Site Identity Manager instance |
| 46 | 54 | * |
| @@ -121,9 +129,10 @@ | ||
| 121 | 129 | [ |
| 122 | 130 | [ |
| 123 | 131 | 'methods' => 'GET', |
| 124 | 132 | 'callback' => [$this, 'get_settings'], |
| 125 | - 'permission_callback' => [$this, 'check_permissions'] | |
| 133 | + 'permission_callback' => [$this, 'check_permissions'], | |
| 134 | + 'args' => $this->get_context_route_args() | |
| 126 | 135 | ], |
| 127 | 136 | [ |
| 128 | 137 | 'methods' => 'POST', |
| 129 | 138 | 'callback' => [$this, 'update_settings'], |
| @@ -194,14 +203,18 @@ | ||
| 194 | 203 | [ |
| 195 | 204 | [ |
| 196 | 205 | 'methods' => 'GET', |
| 197 | 206 | 'callback' => [$this, 'get_robots_txt'], |
| 198 | - 'permission_callback' => [$this, 'check_read_permissions'] | |
| 207 | + // Reading the robots.txt config is a Site Identity operation — | |
| 208 | + // gate it on the module cap, not the generic 'read' cap. | |
| 209 | + 'permission_callback' => [$this, 'check_permissions'] | |
| 199 | 210 | ], |
| 200 | 211 | [ |
| 201 | 212 | 'methods' => 'POST', |
| 202 | 213 | 'callback' => [$this, 'update_robots_txt'], |
| 203 | - 'permission_callback' => [$this, 'check_csrf_permissions'], | |
| 214 | + // Writing robots.txt to the webroot is site-wide — require the | |
| 215 | + // Site Identity management capability, not just edit_posts. | |
| 216 | + 'permission_callback' => [$this, 'check_permissions'], | |
| 204 | 217 | 'args' => $this->get_robots_txt_args() |
| 205 | 218 | ] |
| 206 | 219 | ] |
| 207 | 220 | ); |
| @@ -261,11 +274,8 @@ | ||
| 261 | 274 | ] |
| 262 | 275 | ] |
| 263 | 276 | ); |
| 264 | 277 | |
| 265 | - | |
| 266 | - | |
| 267 | - | |
| 268 | 278 | } |
| 269 | 279 | |
| 270 | 280 | /** |
| 271 | 281 | * Get site identity settings |
| @@ -272,14 +282,19 @@ | ||
| 272 | 282 | * |
| 273 | 283 | * @since 1.0.0 |
| 274 | 284 | * |
| 275 | 285 | * @param WP_REST_Request $request Request object |
| 276 | - * @return WP_REST_Response Response object | |
| 286 | + * @return WP_REST_Response|WP_Error Response object, or the context error | |
| 277 | 287 | */ |
| 278 | - public function get_settings(WP_REST_Request $request): WP_REST_Response { | |
| 288 | + public function get_settings(WP_REST_Request $request) { | |
| 279 | 289 | try { |
| 280 | - $context_type = $request->get_param('context_type') ?? 'site'; | |
| 281 | - $context_id = $request->get_param('context_id'); | |
| 290 | + // SECURITY: the settings are stored per context, so the object has | |
| 291 | + // to be authorised before it is read (#385). | |
| 292 | + $context = $this->resolve_request_context($request); | |
| 293 | + if (is_wp_error($context)) { | |
| 294 | + return $context; | |
| 295 | + } | |
| 296 | + [$context_type, $context_id] = $context; | |
| 282 | 297 | |
| 283 | 298 | // Get settings from Site Identity Manager |
| 284 | 299 | $settings = $this->identity_manager->get_settings($context_type, $context_id); |
| 285 | 300 | |
| @@ -315,11 +330,17 @@ | ||
| 315 | 330 | */ |
| 316 | 331 | public function update_settings(WP_REST_Request $request) { |
| 317 | 332 | try { |
| 318 | 333 | $settings = $request->get_param('settings'); |
| 319 | - $context_type = $request->get_param('context_type') ?? 'site'; | |
| 320 | - $context_id = $request->get_param('context_id'); | |
| 321 | 334 | |
| 335 | + // SECURITY: this write is keyed by the context, so the object has to | |
| 336 | + // be authorised before anything is persisted (#385). | |
| 337 | + $context = $this->resolve_request_context($request); | |
| 338 | + if (is_wp_error($context)) { | |
| 339 | + return $context; | |
| 340 | + } | |
| 341 | + [$context_type, $context_id] = $context; | |
| 342 | + | |
| 322 | 343 | // Validate settings |
| 323 | 344 | if (empty($settings) || !is_array($settings)) { |
| 324 | 345 | return new WP_Error( |
| 325 | 346 | 'invalid_settings', |
| @@ -348,13 +369,35 @@ | ||
| 348 | 369 | |
| 349 | 370 | if (!$update_result) { |
| 350 | 371 | return new WP_Error( |
| 351 | 372 | 'update_failed', |
| 352 | - 'Failed to update site identity settings', | |
| 353 | - ['status' => 500] | |
| 373 | + $this->describe_save_failure('Failed to update site identity settings'), | |
| 374 | + [ | |
| 375 | + 'status' => 500, | |
| 376 | + 'failure_code' => $this->identity_manager->get_last_save_error_code() | |
| 377 | + ] | |
| 354 | 378 | ); |
| 355 | 379 | } |
| 356 | 380 | |
| 381 | + // If this save changed the robots.txt content/toggle and a physical | |
| 382 | + // robots.txt exists, keep it in lockstep. The web server serves that | |
| 383 | + // static file directly (bypassing the robots_txt filter), so without | |
| 384 | + // this the file goes stale and /robots.txt shows the old content | |
| 385 | + // while the textarea shows the new — regardless of what the frontend | |
| 386 | + // believed about the file's existence. | |
| 387 | + // | |
| 388 | + // ai_crawler_rules counts as a robots.txt change even though it is | |
| 389 | + // not the body: the directives it produces are composed into the | |
| 390 | + // served output at render time, so a physical file left alone here | |
| 391 | + // would keep serving the previous allow/block set (#657). | |
| 392 | + if ($context_type === 'site' | |
| 393 | + && (array_key_exists('robots_txt_content', $settings) | |
| 394 | + || array_key_exists('robots_txt_enabled', $settings) | |
| 395 | + || array_key_exists('ai_crawler_rules', $settings)) | |
| 396 | + ) { | |
| 397 | + $this->identity_manager->sync_robots_txt_file(); | |
| 398 | + } | |
| 399 | + | |
| 357 | 400 | // Get updated settings |
| 358 | 401 | $updated_settings = $this->identity_manager->get_settings($context_type, $context_id); |
| 359 | 402 | |
| 360 | 403 | return new WP_REST_Response([ |
| @@ -367,9 +410,9 @@ | ||
| 367 | 410 | ], |
| 368 | 411 | 'message' => 'Site identity settings updated successfully' |
| 369 | 412 | ], 200); |
| 370 | 413 | |
| 371 | - } catch (\Exception $e) { | |
| 414 | + } catch (\Throwable $e) { | |
| 372 | 415 | return new WP_Error( |
| 373 | 416 | 'update_failed', |
| 374 | 417 | 'Settings update failed: ' . $e->getMessage(), |
| 375 | 418 | ['status' => 500] |
| @@ -513,8 +556,39 @@ | ||
| 513 | 556 | |
| 514 | 557 | // Generate robots.txt using Site Identity Manager |
| 515 | 558 | $robots_data = $this->identity_manager->generate_robots_txt($custom_rules); |
| 516 | 559 | |
| 560 | + // The editor shows the body that is actually being served (physical | |
| 561 | + // file if present, else the effective content) — header-stripped so | |
| 562 | + // the auto-generated comment/timestamp never lands in the textarea. | |
| 563 | + $robots_data['content'] = $this->identity_manager->get_served_robots_body(); | |
| 564 | + | |
| 565 | + // Keep `rules` describing that same body. generate_robots_txt() | |
| 566 | + // returned the rules it generated, which stopped matching `content` | |
| 567 | + // the moment a stored override or a physical file supplied it. | |
| 568 | + $robots_data['rules'] = $this->identity_manager->parse_robots_txt_rules($robots_data['content']); | |
| 569 | + | |
| 570 | + // How /robots.txt is actually delivered right now, so the screen can | |
| 571 | + // show the served output next to the editable body and flag a | |
| 572 | + // physical file in the web root that has drifted from the settings. | |
| 573 | + $robots_data['effective'] = $this->identity_manager->get_robots_txt_delivery(); | |
| 574 | + | |
| 575 | + // The per-agent AI crawler surface (#657). The registry ships with | |
| 576 | + // the response rather than being duplicated in the bundle, so a | |
| 577 | + // crawler added by the `thinkrank_ai_crawlers` filter appears in | |
| 578 | + // the UI without a rebuild. Rules are returned normalised, so a | |
| 579 | + // crawler with nothing stored comes back explicitly allowed rather | |
| 580 | + // than as an absence the client has to interpret. | |
| 581 | + $settings = $this->identity_manager->get_settings('site'); | |
| 582 | + $rules = \ThinkRank\SEO\AI_Crawlers::normalize_rules($settings['ai_crawler_rules'] ?? []); | |
| 583 | + | |
| 584 | + $robots_data['ai_crawlers'] = \ThinkRank\SEO\AI_Crawlers::for_display(); | |
| 585 | + $robots_data['ai_crawler_rules'] = []; | |
| 586 | + | |
| 587 | + foreach ($robots_data['ai_crawlers'] as $agent) { | |
| 588 | + $robots_data['ai_crawler_rules'][$agent['slug']] = $rules[$agent['slug']] ?? 'allow'; | |
| 589 | + } | |
| 590 | + | |
| 517 | 591 | return new WP_REST_Response([ |
| 518 | 592 | 'success' => true, |
| 519 | 593 | 'data' => $robots_data, |
| 520 | 594 | 'message' => 'Robots.txt data retrieved successfully' |
| @@ -548,8 +622,19 @@ | ||
| 548 | 622 | } |
| 549 | 623 | $custom_rules = $request->get_param('custom_rules') ?? []; |
| 550 | 624 | $enable_management = $request->get_param('enable_management') ?? true; |
| 551 | 625 | |
| 626 | + // Two callers share this route: | |
| 627 | + // - "Generate" rebuilds the content from rules and adopts it as the | |
| 628 | + // stored textarea content ($regenerate = true). | |
| 629 | + // - A plain save that only needs the physical file re-synced to the | |
| 630 | + // already-stored textarea content ($regenerate = false). | |
| 631 | + // Default to true so the historical Generate contract is unchanged. | |
| 632 | + $regenerate = $request->get_param('regenerate'); | |
| 633 | + if ($regenerate === null) { | |
| 634 | + $regenerate = true; | |
| 635 | + } | |
| 636 | + | |
| 552 | 637 | // Validate custom rules format |
| 553 | 638 | if (!is_array($custom_rules)) { |
| 554 | 639 | return new WP_Error( |
| 555 | 640 | 'invalid_rules', |
| @@ -557,51 +642,79 @@ | ||
| 557 | 642 | ['status' => 400] |
| 558 | 643 | ); |
| 559 | 644 | } |
| 560 | 645 | |
| 561 | - // Generate and validate robots.txt | |
| 562 | - $robots_data = $this->identity_manager->generate_robots_txt($custom_rules); | |
| 646 | + $settings = ['robots_txt_enabled' => $enable_management]; | |
| 563 | 647 | |
| 564 | - if (!empty($robots_data['validation']['errors'])) { | |
| 565 | - return new WP_Error( | |
| 566 | - 'validation_failed', | |
| 567 | - 'Robots.txt validation failed', | |
| 568 | - [ | |
| 569 | - 'status' => 400, | |
| 570 | - 'validation_errors' => $robots_data['validation']['errors'] | |
| 571 | - ] | |
| 572 | - ); | |
| 648 | + // Per-agent AI crawler rules (#657). Only written when the caller | |
| 649 | + // sends them: this route is also the plain "re-sync the file" save, | |
| 650 | + // and defaulting a missing parameter to an empty map there would | |
| 651 | + // unblock every crawler the site had blocked. | |
| 652 | + $ai_rules = $request->get_param('ai_crawler_rules'); | |
| 653 | + if (null !== $ai_rules) { | |
| 654 | + $settings['ai_crawler_rules'] = \ThinkRank\SEO\AI_Crawlers::normalize_rules($ai_rules); | |
| 573 | 655 | } |
| 574 | 656 | |
| 575 | - // Update robots.txt settings | |
| 576 | - $settings = [ | |
| 577 | - 'robots_txt_enabled' => $enable_management, | |
| 578 | - 'custom_robots_rules' => $custom_rules, | |
| 579 | - 'robots_txt_content' => $robots_data['content'] | |
| 580 | - ]; | |
| 657 | + if ($regenerate) { | |
| 658 | + // Generate and validate robots.txt from the rules. | |
| 659 | + $robots_data = $this->identity_manager->generate_robots_txt($custom_rules); | |
| 581 | 660 | |
| 661 | + if (!empty($robots_data['validation']['errors'])) { | |
| 662 | + return new WP_Error( | |
| 663 | + 'validation_failed', | |
| 664 | + 'Robots.txt validation failed', | |
| 665 | + [ | |
| 666 | + 'status' => 400, | |
| 667 | + 'validation_errors' => $robots_data['validation']['errors'] | |
| 668 | + ] | |
| 669 | + ); | |
| 670 | + } | |
| 671 | + | |
| 672 | + // Adopt the freshly generated content as the stored source. | |
| 673 | + $settings['custom_robots_rules'] = $custom_rules; | |
| 674 | + $settings['robots_txt_content'] = $robots_data['content']; | |
| 675 | + } | |
| 676 | + | |
| 582 | 677 | $update_result = $this->identity_manager->save_settings('site', null, $settings); |
| 583 | 678 | |
| 584 | 679 | if (!$update_result) { |
| 585 | 680 | return new WP_Error( |
| 586 | 681 | 'update_failed', |
| 587 | - 'Failed to update robots.txt settings', | |
| 588 | - ['status' => 500] | |
| 682 | + $this->describe_save_failure('Failed to update robots.txt settings'), | |
| 683 | + [ | |
| 684 | + 'status' => 500, | |
| 685 | + 'failure_code' => $this->identity_manager->get_last_save_error_code() | |
| 686 | + ] | |
| 589 | 687 | ); |
| 590 | 688 | } |
| 591 | 689 | |
| 592 | - // Write robots.txt file to filesystem if management is enabled | |
| 690 | + // Effective content = the stored textarea content when set, else the | |
| 691 | + // auto-generated body. This is exactly what the live /robots.txt | |
| 692 | + // serves, so writing it here keeps the physical file in lockstep. | |
| 693 | + $effective_content = $this->identity_manager->render_robots_txt(); | |
| 694 | + | |
| 695 | + // Write robots.txt file to filesystem if management is enabled. | |
| 593 | 696 | $file_write_result = ['success' => false, 'message' => 'File writing disabled']; |
| 594 | - if ($enable_management && !empty($robots_data['content'])) { | |
| 697 | + if ($enable_management && $effective_content !== '') { | |
| 595 | 698 | $write_to_file = $request->get_param('write_to_file') ?? true; |
| 596 | 699 | |
| 597 | 700 | if ($write_to_file) { |
| 598 | 701 | $file_write_result = $this->identity_manager->write_robots_txt( |
| 599 | - $robots_data['content'] | |
| 702 | + $effective_content | |
| 600 | 703 | ); |
| 601 | 704 | } |
| 602 | 705 | } |
| 603 | 706 | |
| 707 | + $robots_data = $robots_data ?? []; | |
| 708 | + // Return the header-stripped body so the client textarea reflects | |
| 709 | + // exactly what it should hold (the header is added only at render). | |
| 710 | + $robots_data['content'] = $this->identity_manager->get_served_robots_body(); | |
| 711 | + $robots_data['rules'] = $this->identity_manager->parse_robots_txt_rules($robots_data['content']); | |
| 712 | + | |
| 713 | + // Re-read delivery after the write above so the screen reflects the | |
| 714 | + // file that now exists rather than the state it was in on load. | |
| 715 | + $robots_data['effective'] = $this->identity_manager->get_robots_txt_delivery(); | |
| 716 | + | |
| 604 | 717 | return new WP_REST_Response([ |
| 605 | 718 | 'success' => true, |
| 606 | 719 | 'data' => [ |
| 607 | 720 | 'robots_data' => $robots_data, |
| @@ -612,9 +725,9 @@ | ||
| 612 | 725 | ? 'Robots.txt configuration updated and file written successfully' |
| 613 | 726 | : 'Robots.txt configuration updated (file not written: ' . $file_write_result['message'] . ')' |
| 614 | 727 | ], 200); |
| 615 | 728 | |
| 616 | - } catch (\Exception $e) { | |
| 729 | + } catch (\Throwable $e) { | |
| 617 | 730 | return new WP_Error( |
| 618 | 731 | 'update_failed', |
| 619 | 732 | 'Robots.txt update failed: ' . $e->getMessage(), |
| 620 | 733 | ['status' => 500] |
| @@ -687,25 +800,24 @@ | ||
| 687 | 800 | ['status' => 400] |
| 688 | 801 | ); |
| 689 | 802 | } |
| 690 | 803 | |
| 691 | - // Validate required site data fields | |
| 692 | - $required_fields = ['site_name', 'site_description', 'tagline']; | |
| 693 | - foreach ($required_fields as $field) { | |
| 694 | - if (empty($site_data[$field])) { | |
| 695 | - return new WP_Error( | |
| 696 | - 'missing_field', | |
| 697 | - "Required field '{$field}' is missing or empty", | |
| 698 | - ['status' => 400] | |
| 699 | - ); | |
| 700 | - } | |
| 804 | + // Only the site name is required. An empty description or tagline | |
| 805 | + // is a valid state — and exactly when AI help is most useful — so | |
| 806 | + // let the optimizer generate them instead of rejecting the request. | |
| 807 | + if (empty($site_data['site_name'])) { | |
| 808 | + return new WP_Error( | |
| 809 | + 'missing_field', | |
| 810 | + __('Site name is required to run AI optimization.', 'thinkrank'), | |
| 811 | + ['status' => 400] | |
| 812 | + ); | |
| 701 | 813 | } |
| 702 | 814 | |
| 703 | - // Sanitize site data | |
| 815 | + // Sanitize site data (description/tagline may be empty by design) | |
| 704 | 816 | $sanitized_site_data = [ |
| 705 | 817 | 'site_name' => sanitize_text_field($site_data['site_name']), |
| 706 | - 'site_description' => sanitize_textarea_field($site_data['site_description']), | |
| 707 | - 'tagline' => sanitize_text_field($site_data['tagline']), | |
| 818 | + 'site_description' => sanitize_textarea_field($site_data['site_description'] ?? ''), | |
| 819 | + 'tagline' => sanitize_text_field($site_data['tagline'] ?? ''), | |
| 708 | 820 | 'default_meta_description' => sanitize_textarea_field($site_data['default_meta_description'] ?? '') |
| 709 | 821 | ]; |
| 710 | 822 | |
| 711 | 823 | // Sanitize options |
| @@ -814,12 +926,14 @@ | ||
| 814 | 926 | */ |
| 815 | 927 | public function validate_identity_settings(WP_REST_Request $request): WP_REST_Response { |
| 816 | 928 | try { |
| 817 | 929 | $settings = $request->get_param('settings'); |
| 818 | - $tab_context = $request->get_param('tab_context') ?? ''; | |
| 930 | + $tab_context = $request->get_param('tab_context'); | |
| 819 | 931 | |
| 820 | - // Validate settings using Site Identity Manager with tab context | |
| 821 | - $validation = $this->identity_manager->validate_settings($settings ?? [], $tab_context); | |
| 932 | + // Validate settings using Site Identity Manager with tab context. | |
| 933 | + // `settings` is registered required, so REST rejects a missing value | |
| 934 | + // before this point and the old `?? []` fallback was unreachable. | |
| 935 | + $validation = $this->identity_manager->validate_settings($settings, $tab_context); | |
| 822 | 936 | |
| 823 | 937 | return new WP_REST_Response([ |
| 824 | 938 | 'success' => true, |
| 825 | 939 | 'data' => $validation, |
| @@ -833,12 +947,8 @@ | ||
| 833 | 947 | ], 500); |
| 834 | 948 | } |
| 835 | 949 | } |
| 836 | 950 | |
| 837 | - | |
| 838 | - | |
| 839 | - | |
| 840 | - | |
| 841 | 951 | /** |
| 842 | 952 | * Permission callbacks |
| 843 | 953 | */ |
| 844 | 954 | |
| @@ -849,36 +959,10 @@ | ||
| 849 | 959 | * |
| 850 | 960 | * @return bool Permission status |
| 851 | 961 | */ |
| 852 | 962 | public function check_permissions(): bool { |
| 853 | - return current_user_can('manage_options'); | |
| 963 | + return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_site_identity'); | |
| 854 | 964 | } |
| 855 | - | |
| 856 | - /** | |
| 857 | - * Helper methods | |
| 858 | - */ | |
| 859 | - | |
| 860 | - /** | |
| 861 | - * Validate context type and ID | |
| 862 | - * | |
| 863 | - * @since 1.0.0 | |
| 864 | - * | |
| 865 | - * @param string $context_type Context type | |
| 866 | - * @param int|null $context_id Context ID | |
| 867 | - * @return bool Validation status | |
| 868 | - */ | |
| 869 | - private function validate_context(string $context_type): bool { | |
| 870 | - // Only support 'site' context in development stage | |
| 871 | - return $context_type === 'site'; | |
| 872 | - } | |
| 873 | - | |
| 874 | - /** | |
| 875 | - * Get available title templates | |
| 876 | - * | |
| 877 | - * @since 1.0.0 | |
| 878 | - * | |
| 879 | - * @return array Title templates | |
| 880 | - */ | |
| 881 | 965 | private function get_available_title_templates(): array { |
| 882 | 966 | return [ |
| 883 | 967 | 'default' => [ |
| 884 | 968 | 'name' => 'Default', |
| @@ -908,8 +992,28 @@ | ||
| 908 | 992 | * Argument validation methods |
| 909 | 993 | */ |
| 910 | 994 | |
| 911 | 995 | /** |
| 996 | + * Build a save-failure message that names the actual cause. | |
| 997 | + * | |
| 998 | + * The manager knows why the save failed — missing settings table, rejected | |
| 999 | + * INSERT with the MySQL error attached — and used to write that to the | |
| 1000 | + * error log and throw it away, leaving the client a fixed string that told | |
| 1001 | + * nobody anything. Append the reason so the response is diagnosable on its | |
| 1002 | + * own. Status stays 500: a rejected INSERT is a server-side failure. | |
| 1003 | + * | |
| 1004 | + * @since 1.32.1 | |
| 1005 | + * | |
| 1006 | + * @param string $fallback Message to use when no reason was recorded. | |
| 1007 | + * @return string Failure message. | |
| 1008 | + */ | |
| 1009 | + private function describe_save_failure(string $fallback): string { | |
| 1010 | + $reason = $this->identity_manager->get_last_save_error(); | |
| 1011 | + | |
| 1012 | + return '' !== $reason ? $fallback . ': ' . $reason : $fallback; | |
| 1013 | + } | |
| 1014 | + | |
| 1015 | + /** | |
| 912 | 1016 | * Get arguments for settings endpoints |
| 913 | 1017 | * |
| 914 | 1018 | * @since 1.0.0 |
| 915 | 1019 | * |
| @@ -949,9 +1053,12 @@ | ||
| 949 | 1053 | return [ |
| 950 | 1054 | 'template_name' => [ |
| 951 | 1055 | 'required' => false, |
| 952 | 1056 | 'type' => 'string', |
| 953 | - 'enum' => ['default', 'simple', 'reverse', 'category', 'author'], | |
| 1057 | + // Must match get_available_title_templates(), which returns | |
| 1058 | + // 'default' and nothing else. The extra names advertised | |
| 1059 | + // templates the resolver has never been able to produce. | |
| 1060 | + 'enum' => ['default'], | |
| 954 | 1061 | 'default' => 'default', |
| 955 | 1062 | 'description' => 'Title template to use' |
| 956 | 1063 | ], |
| 957 | 1064 | 'data' => [ |
| @@ -980,9 +1087,11 @@ | ||
| 980 | 1087 | return [ |
| 981 | 1088 | 'breadcrumb_type' => [ |
| 982 | 1089 | 'required' => false, |
| 983 | 1090 | 'type' => 'string', |
| 984 | - 'enum' => ['hierarchical', 'taxonomy', 'path', 'custom'], | |
| 1091 | + // Must match get_available_breadcrumb_types(), which returns | |
| 1092 | + // 'hierarchical' and nothing else. | |
| 1093 | + 'enum' => ['hierarchical'], | |
| 985 | 1094 | 'default' => 'hierarchical', |
| 986 | 1095 | 'description' => 'Type of breadcrumb navigation to generate' |
| 987 | 1096 | ], |
| 988 | 1097 | 'options' => [ |
| @@ -1014,8 +1123,35 @@ | ||
| 1014 | 1123 | 'required' => false, |
| 1015 | 1124 | 'type' => 'boolean', |
| 1016 | 1125 | 'default' => true, |
| 1017 | 1126 | 'description' => 'Enable automatic robots.txt management' |
| 1127 | + ], | |
| 1128 | + 'regenerate' => [ | |
| 1129 | + 'required' => false, | |
| 1130 | + 'type' => 'boolean', | |
| 1131 | + 'default' => true, | |
| 1132 | + 'description' => 'Rebuild content from rules (Generate). When false, only re-sync the physical file to the stored content.' | |
| 1133 | + ], | |
| 1134 | + // The handler reads this and the route never declared it, so it | |
| 1135 | + // arrived as whatever string the client sent. RobotsManagement.js | |
| 1136 | + // sends it, and "false" is a non-empty string — truthy — so the | |
| 1137 | + // file was written when the caller had asked it not to be. ("0" is | |
| 1138 | + // falsy, which is why the failure was asymmetric.) Registering it | |
| 1139 | + // gets core's boolean coercion (#394). | |
| 1140 | + 'ai_crawler_rules' => [ | |
| 1141 | + 'required' => false, | |
| 1142 | + 'type' => 'object', | |
| 1143 | + 'description' => 'Per-agent AI crawler rules, keyed by crawler slug, each "allow" or "block".', | |
| 1144 | + 'additionalProperties' => [ | |
| 1145 | + 'type' => 'string', | |
| 1146 | + 'enum' => ['allow', 'block'], | |
| 1147 | + ], | |
| 1148 | + ], | |
| 1149 | + 'write_to_file' => [ | |
| 1150 | + 'required' => false, | |
| 1151 | + 'type' => 'boolean', | |
| 1152 | + 'default' => true, | |
| 1153 | + 'description' => 'Write the generated content to the physical robots.txt file.' | |
| 1018 | 1154 | ] |
| 1019 | 1155 | ]; |
| 1020 | 1156 | } |
| 1021 | 1157 | |
| @@ -1031,8 +1167,16 @@ | ||
| 1031 | 1167 | 'identity_data' => [ |
| 1032 | 1168 | 'required' => true, |
| 1033 | 1169 | 'type' => 'object', |
| 1034 | 1170 | 'description' => 'Site identity data to optimize' |
| 1171 | + ], | |
| 1172 | + // Read by optimize_site_identity(); previously unregistered, so it | |
| 1173 | + // never appeared in the published schema. | |
| 1174 | + 'options' => [ | |
| 1175 | + 'required' => false, | |
| 1176 | + 'type' => 'object', | |
| 1177 | + 'default' => [], | |
| 1178 | + 'description' => 'Additional optimization options' | |
| 1035 | 1179 | ] |
| 1036 | 1180 | ]; |
| 1037 | 1181 | } |
| 1038 | 1182 | |
| @@ -1179,8 +1323,18 @@ | ||
| 1179 | 1323 | 'settings' => [ |
| 1180 | 1324 | 'required' => true, |
| 1181 | 1325 | 'type' => 'object', |
| 1182 | 1326 | 'description' => 'Settings to validate' |
| 1327 | + ], | |
| 1328 | + // Read by validate_identity_settings() to scope validation to one | |
| 1329 | + // tab; it was never registered, so it was absent from the published | |
| 1330 | + // schema and got no type or sanitization. | |
| 1331 | + 'tab_context' => [ | |
| 1332 | + 'required' => false, | |
| 1333 | + 'type' => 'string', | |
| 1334 | + 'default' => '', | |
| 1335 | + 'sanitize_callback' => 'sanitize_key', | |
| 1336 | + 'description' => 'Limit validation to a single settings tab' | |
| 1183 | 1337 | ] |
| 1184 | 1338 | ]; |
| 1185 | 1339 | } |
| 1186 | 1340 | |
| @@ -1195,9 +1349,9 @@ | ||
| 1195 | 1349 | $rate_key = "thinkrank_robots_rate_{$user_id}"; |
| 1196 | 1350 | |
| 1197 | 1351 | $requests = get_transient($rate_key) ?: 0; |
| 1198 | 1352 | |
| 1199 | - if ($requests >= 3) { // Max 3 requests per 5 minutes | |
| 1353 | + if ($requests >= 20) { // Max 20 requests per 5 minutes | |
| 1200 | 1354 | return false; |
| 1201 | 1355 | } |
| 1202 | 1356 | |
| 1203 | 1357 | set_transient($rate_key, $requests + 1, 5 * MINUTE_IN_SECONDS); |