| @@ -20,8 +20,10 @@ | ||
| 20 | 20 | use ThinkRank\Admin\Metabox_Manager; |
| 21 | 21 | use ThinkRank\Admin\Elementor_Metabox; |
| 22 | 22 | use ThinkRank\Admin\Oxygen_Metabox; |
| 23 | 23 | use ThinkRank\Admin\Divi_Metabox; |
| 24 | +use ThinkRank\Admin\Bricks_Metabox; | |
| 25 | +use ThinkRank\Admin\Beaver_Metabox; | |
| 24 | 26 | use ThinkRank\Admin\Bulk_Action_Manager; |
| 25 | 27 | use ThinkRank\Admin\Post_List_Filters; |
| 26 | 28 | |
| 27 | 29 | // Prevent direct access |
| @@ -80,8 +82,22 @@ | ||
| 80 | 82 | */ |
| 81 | 83 | private Divi_Metabox $divi_metabox; |
| 82 | 84 | |
| 83 | 85 | /** |
| 86 | + * Bricks builder metabox integration instance | |
| 87 | + * | |
| 88 | + * @var Bricks_Metabox | |
| 89 | + */ | |
| 90 | + private Bricks_Metabox $bricks_metabox; | |
| 91 | + | |
| 92 | + /** | |
| 93 | + * Beaver Builder metabox integration | |
| 94 | + * | |
| 95 | + * @var Beaver_Metabox | |
| 96 | + */ | |
| 97 | + private Beaver_Metabox $beaver_metabox; | |
| 98 | + | |
| 99 | + /** | |
| 84 | 100 | * Post list columns instance |
| 85 | 101 | * |
| 86 | 102 | * @var Post_List_Columns |
| 87 | 103 | */ |
| @@ -94,8 +110,15 @@ | ||
| 94 | 110 | */ |
| 95 | 111 | private Focus_Keyword_Ajax $focus_keyword_ajax; |
| 96 | 112 | |
| 97 | 113 | /** |
| 114 | + * SEO Quick Edit modal AJAX handler instance | |
| 115 | + * | |
| 116 | + * @var Seo_Quick_Edit_Ajax | |
| 117 | + */ | |
| 118 | + private Seo_Quick_Edit_Ajax $seo_quick_edit_ajax; | |
| 119 | + | |
| 120 | + /** | |
| 98 | 121 | * Bulk action manager instance |
| 99 | 122 | * |
| 100 | 123 | * @var Bulk_Action_Manager |
| 101 | 124 | */ |
| @@ -127,10 +150,13 @@ | ||
| 127 | 150 | $this->metabox_manager = new Metabox_Manager($this->settings); |
| 128 | 151 | $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager); |
| 129 | 152 | $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager); |
| 130 | 153 | $this->divi_metabox = new Divi_Metabox($this->metabox_manager); |
| 154 | + $this->bricks_metabox = new Bricks_Metabox($this->metabox_manager); | |
| 155 | + $this->beaver_metabox = new Beaver_Metabox($this->metabox_manager); | |
| 131 | 156 | $this->post_list_columns = new Post_List_Columns(); |
| 132 | 157 | $this->focus_keyword_ajax = new Focus_Keyword_Ajax(); |
| 158 | + $this->seo_quick_edit_ajax = new Seo_Quick_Edit_Ajax(); | |
| 133 | 159 | $this->bulk_action_manager = new Bulk_Action_Manager(); |
| 134 | 160 | $this->post_list_filters = new Post_List_Filters(); |
| 135 | 161 | } |
| 136 | 162 | |
| @@ -163,8 +189,16 @@ | ||
| 163 | 189 | // Initialize Divi Visual Builder integration (hooks gate on the VB |
| 164 | 190 | // request, so they no-op without Divi) |
| 165 | 191 | $this->divi_metabox->init(); |
| 166 | 192 | |
| 193 | + // Initialize Bricks builder integration (hooks gate on Bricks' own | |
| 194 | + // builder detector, so they no-op without Bricks) | |
| 195 | + $this->bricks_metabox->init(); | |
| 196 | + | |
| 197 | + // Initialize Beaver Builder integration (hooks gate on Beaver Builder's | |
| 198 | + // own builder detector, so they no-op without Beaver Builder) | |
| 199 | + $this->beaver_metabox->init(); | |
| 200 | + | |
| 167 | 201 | // Initialize post list columns |
| 168 | 202 | $this->post_list_columns->init(); |
| 169 | 203 | |
| 170 | 204 | // Initialize focus keyword AJAX handler |
| @@ -169,8 +203,11 @@ | ||
| 169 | 203 | |
| 170 | 204 | // Initialize focus keyword AJAX handler |
| 171 | 205 | $this->focus_keyword_ajax->init(); |
| 172 | 206 | |
| 207 | + // Initialize SEO Quick Edit modal AJAX handler | |
| 208 | + $this->seo_quick_edit_ajax->init(); | |
| 209 | + | |
| 173 | 210 | // Initialize Bulk Action Manager |
| 174 | 211 | $this->bulk_action_manager->init(); |
| 175 | 212 | |
| 176 | 213 | // Initialize Post List Filters |
| @@ -249,13 +286,33 @@ | ||
| 249 | 286 | 'thinkrank-settings', |
| 250 | 287 | [$this, 'render_settings_page'] |
| 251 | 288 | ); |
| 252 | 289 | |
| 253 | - // Migration page — re-run SEO data imports from other plugins after | |
| 254 | - // setup. Hidden by default; shown only when the "Enable Migration Tools" | |
| 255 | - // advanced setting is on. Capability matches the import REST endpoints | |
| 256 | - // (`manage_options`) so the UI and API stay in agreement. | |
| 257 | - if (Settings::instance()->get('enable_migration_tools', false)) { | |
| 290 | + // Two separate screens, one per setting, so each menu item appears | |
| 291 | + // exactly when its own feature is on. They shared a page (and therefore | |
| 292 | + // a menu item) until #228: with one route, turning Import / Export off | |
| 293 | + // still left "Import / Export" in the sidebar whenever Migration Tools | |
| 294 | + // happened to be on, which is the opposite of what the switch promises. | |
| 295 | + // | |
| 296 | + // Capability matches the import/export REST endpoints (`manage_options`) | |
| 297 | + // so the UI and API stay in agreement. | |
| 298 | + | |
| 299 | + // ThinkRank's own data, out to a file and back. Off by default. | |
| 300 | + if ((bool) Settings::instance()->get('enable_import_export', false)) { | |
| 301 | + $this->pages['import-export'] = add_submenu_page( | |
| 302 | + 'thinkrank', | |
| 303 | + __('Import / Export', 'thinkrank'), | |
| 304 | + __('Import / Export', 'thinkrank'), | |
| 305 | + 'manage_options', | |
| 306 | + 'thinkrank-import-export', | |
| 307 | + [$this, 'render_import_export_page'] | |
| 308 | + ); | |
| 309 | + } | |
| 310 | + | |
| 311 | + // Importing FROM another SEO plugin. Off by default. Slug kept as | |
| 312 | + // thinkrank-migration so existing links, bookmarks and the docs keep | |
| 313 | + // resolving to the migration screen they always meant. | |
| 314 | + if ((bool) Settings::instance()->get('enable_migration_tools', false)) { | |
| 258 | 315 | $this->pages['migration'] = add_submenu_page( |
| 259 | 316 | 'thinkrank', |
| 260 | 317 | __('Migration', 'thinkrank'), |
| 261 | 318 | __('Migration', 'thinkrank'), |
| @@ -370,8 +427,12 @@ | ||
| 370 | 427 | 'capabilities' => $this->get_user_capabilities(), |
| 371 | 428 | 'settings' => $this->get_admin_settings(), |
| 372 | 429 | 'i18n' => $this->get_i18n_strings(), |
| 373 | 430 | 'isAdmin' => current_user_can('manage_options'), |
| 431 | + // One flag per half of the Import / Export page: the "import from | |
| 432 | + // another SEO plugin" section, and ThinkRank's own export/restore. | |
| 433 | + 'migrationToolsEnabled' => (bool) $this->settings->get('enable_migration_tools', false), | |
| 434 | + 'importExportEnabled' => (bool) $this->settings->get('enable_import_export', false), | |
| 374 | 435 | // Whether any AI provider API key is configured — used to gate |
| 375 | 436 | // "Generate with AI" buttons in the UI |
| 376 | 437 | 'aiConfigured' => $this->is_ai_configured(), |
| 377 | 438 | // Plugin version - directly available without API call |
| @@ -391,13 +452,10 @@ | ||
| 391 | 452 | 'sectionCaps' => Capability_Manager::section_map(), |
| 392 | 453 | 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES), |
| 393 | 454 | // Pro detection flag |
| 394 | 455 | 'isPro' => Plan_Config::is_pro(), |
| 395 | - // Data update frequency (Pro: daily, Free: every 3 days) | |
| 396 | - 'dataUpdateFrequency' => Plan_Config::is_pro() ? 'daily' : '3days', | |
| 397 | - // Per-feature capability maps. Mirrors PHP Plan_Config so JS | |
| 398 | - // never has to ask "is the user Pro?" — it asks "can the user X?". | |
| 399 | - 'emailReport' => Plan_Config::email_report(), | |
| 456 | + // NB: no per-feature capability maps here; each screen reads its | |
| 457 | + // own state over REST, so a localized copy cannot drift from it. | |
| 400 | 458 | // MCP (Model Context Protocol) connection details for the MCP page. |
| 401 | 459 | 'mcp' => $this->get_mcp_globals(), |
| 402 | 460 | // Google OAuth: JS only ever gets a nonce-signed admin-post URL. |
| 403 | 461 | // The consent URL, client ID, and scopes are assembled by the proxy, |
| @@ -408,8 +466,14 @@ | ||
| 408 | 466 | // 'contract' (upgraded off the old token flow) or |
| 409 | 467 | // 'credentials' (stored tokens no longer decryptable). |
| 410 | 468 | 'reconnectReason' => (string) get_option('thinkrank_google_reconnect_required', ''), |
| 411 | 469 | ], |
| 470 | + // Setup Wizard state — the dashboard Quick Access widget surfaces a | |
| 471 | + // "Complete Setup" shortcut while onboarding is unfinished. | |
| 472 | + 'setupWizard' => [ | |
| 473 | + 'completed' => (bool) get_option(Setup_Wizard::OPT_COMPLETED, false), | |
| 474 | + 'url' => admin_url('admin.php?page=' . Setup_Wizard::PAGE_SLUG), | |
| 475 | + ], | |
| 412 | 476 | ]); |
| 413 | 477 | |
| 414 | 478 | } |
| 415 | 479 | |
| @@ -523,13 +587,22 @@ | ||
| 523 | 587 | ]); |
| 524 | 588 | } |
| 525 | 589 | |
| 526 | 590 | /** |
| 527 | - * Render import/export page | |
| 591 | + * Render the Import / Export page (ThinkRank's own data, out and back). | |
| 528 | 592 | * |
| 593 | + * Defense in depth: the submenu is only registered while the setting is on, | |
| 594 | + * but re-check here so a direct hit on the page URL cannot bypass the gate. | |
| 595 | + * The export REST routes carry their own `manage_options` check, so nothing | |
| 596 | + * is protected by the page alone. | |
| 597 | + * | |
| 529 | 598 | * @return void |
| 530 | 599 | */ |
| 531 | 600 | public function render_import_export_page(): void { |
| 601 | + if (!(bool) Settings::instance()->get('enable_import_export', false)) { | |
| 602 | + wp_die(esc_html__('Import / Export is not enabled.', 'thinkrank')); | |
| 603 | + } | |
| 604 | + | |
| 532 | 605 | $this->render_admin_page('import-export', [ |
| 533 | 606 | 'page_title' => __('Import / Export', 'thinkrank'), |
| 534 | 607 | ]); |
| 535 | 608 | } |
| @@ -534,19 +607,19 @@ | ||
| 534 | 607 | ]); |
| 535 | 608 | } |
| 536 | 609 | |
| 537 | 610 | /** |
| 538 | - * Render the Migration page (re-run SEO data imports). | |
| 611 | + * Render the Migration page (import SEO data from another plugin). | |
| 539 | 612 | * |
| 540 | - * Defense in depth: the submenu is only registered when the setting is on, | |
| 541 | - * but re-check here so a direct hit on the page URL can't bypass the gate. | |
| 613 | + * Same defense in depth as above, against its own setting. | |
| 542 | 614 | * |
| 543 | 615 | * @return void |
| 544 | 616 | */ |
| 545 | 617 | public function render_migration_page(): void { |
| 546 | - if (!Settings::instance()->get('enable_migration_tools', false)) { | |
| 618 | + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) { | |
| 547 | 619 | wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank')); |
| 548 | 620 | } |
| 621 | + | |
| 549 | 622 | $this->render_admin_page('migration', [ |
| 550 | 623 | 'page_title' => __('Migration', 'thinkrank'), |
| 551 | 624 | ]); |
| 552 | 625 | } |
| @@ -713,8 +786,14 @@ | ||
| 713 | 786 | */ |
| 714 | 787 | public function dismiss_notice(): void { |
| 715 | 788 | check_ajax_referer('thinkrank_admin', 'nonce'); |
| 716 | 789 | |
| 790 | + // The nonce proves intent, not authorization — dismissing a site-wide | |
| 791 | + // notice deletes an option, so require a capability as well. | |
| 792 | + if (!current_user_can('edit_posts')) { | |
| 793 | + wp_die(-1, 403); | |
| 794 | + } | |
| 795 | + | |
| 717 | 796 | $notice_type = sanitize_key($_POST['notice_type'] ?? ''); |
| 718 | 797 | |
| 719 | 798 | if ($notice_type === 'welcome') { |
| 720 | 799 | delete_option('thinkrank_show_welcome'); |
| @@ -742,8 +821,9 @@ | ||
| 742 | 821 | * |
| 743 | 822 | * @return string Menu icon |
| 744 | 823 | */ |
| 745 | 824 | private function get_menu_icon(): string { |
| 825 | + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- a data: URI for the menu icon must be base64. | |
| 746 | 826 | return 'data:image/svg+xml;base64,' . base64_encode( |
| 747 | 827 | '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 748 | 828 | <g clipPath="url(#thinkrank-clip)"> |
| 749 | 829 | <g filter="url(#thinkrank-shadow)"> |
| @@ -791,9 +871,9 @@ | ||
| 791 | 871 | */ |
| 792 | 872 | private function get_admin_settings(): array { |
| 793 | 873 | return [ |
| 794 | 874 | |
| 795 | - 'ai_provider' => $this->settings->get('ai_provider', 'openai'), | |
| 875 | + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE), | |
| 796 | 876 | 'cache_duration' => $this->settings->get('cache_duration', 3600), |
| 797 | 877 | ]; |
| 798 | 878 | } |
| 799 | 879 | |
| @@ -925,10 +1005,11 @@ | ||
| 925 | 1005 | 'thinkrank_page_thinkrank-ai-tools', |
| 926 | 1006 | 'thinkrank_page_thinkrank-settings', |
| 927 | 1007 | 'thinkrank_page_thinkrank-usages', |
| 928 | 1008 | 'thinkrank_page_thinkrank-license', |
| 1009 | + 'thinkrank_page_thinkrank-import-export', | |
| 929 | 1010 | 'thinkrank_page_thinkrank-migration' |
| 930 | - ] ) ) { | |
| 1011 | + ] , true) ) { | |
| 931 | 1012 | |
| 932 | 1013 | remove_all_actions( 'user_admin_notices' ); |
| 933 | 1014 | remove_all_actions( 'admin_notices' ); |
| 934 | 1015 | remove_all_actions( 'all_admin_notices' ); |