| @@ -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 | */ |
| @@ -134,8 +150,10 @@ | ||
| 134 | 150 | $this->metabox_manager = new Metabox_Manager($this->settings); |
| 135 | 151 | $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager); |
| 136 | 152 | $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager); |
| 137 | 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); | |
| 138 | 156 | $this->post_list_columns = new Post_List_Columns(); |
| 139 | 157 | $this->focus_keyword_ajax = new Focus_Keyword_Ajax(); |
| 140 | 158 | $this->seo_quick_edit_ajax = new Seo_Quick_Edit_Ajax(); |
| 141 | 159 | $this->bulk_action_manager = new Bulk_Action_Manager(); |
| @@ -171,8 +189,16 @@ | ||
| 171 | 189 | // Initialize Divi Visual Builder integration (hooks gate on the VB |
| 172 | 190 | // request, so they no-op without Divi) |
| 173 | 191 | $this->divi_metabox->init(); |
| 174 | 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 | + | |
| 175 | 201 | // Initialize post list columns |
| 176 | 202 | $this->post_list_columns->init(); |
| 177 | 203 | |
| 178 | 204 | // Initialize focus keyword AJAX handler |
| @@ -260,13 +286,33 @@ | ||
| 260 | 286 | 'thinkrank-settings', |
| 261 | 287 | [$this, 'render_settings_page'] |
| 262 | 288 | ); |
| 263 | 289 | |
| 264 | - // Migration page — re-run SEO data imports from other plugins after | |
| 265 | - // setup. Hidden by default; shown only when the "Enable Migration Tools" | |
| 266 | - // advanced setting is on. Capability matches the import REST endpoints | |
| 267 | - // (`manage_options`) so the UI and API stay in agreement. | |
| 268 | - 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)) { | |
| 269 | 315 | $this->pages['migration'] = add_submenu_page( |
| 270 | 316 | 'thinkrank', |
| 271 | 317 | __('Migration', 'thinkrank'), |
| 272 | 318 | __('Migration', 'thinkrank'), |
| @@ -381,8 +427,12 @@ | ||
| 381 | 427 | 'capabilities' => $this->get_user_capabilities(), |
| 382 | 428 | 'settings' => $this->get_admin_settings(), |
| 383 | 429 | 'i18n' => $this->get_i18n_strings(), |
| 384 | 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), | |
| 385 | 435 | // Whether any AI provider API key is configured — used to gate |
| 386 | 436 | // "Generate with AI" buttons in the UI |
| 387 | 437 | 'aiConfigured' => $this->is_ai_configured(), |
| 388 | 438 | // Plugin version - directly available without API call |
| @@ -402,13 +452,10 @@ | ||
| 402 | 452 | 'sectionCaps' => Capability_Manager::section_map(), |
| 403 | 453 | 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES), |
| 404 | 454 | // Pro detection flag |
| 405 | 455 | 'isPro' => Plan_Config::is_pro(), |
| 406 | - // Data update frequency (Pro: daily, Free: every 3 days) | |
| 407 | - 'dataUpdateFrequency' => Plan_Config::is_pro() ? 'daily' : '3days', | |
| 408 | - // Per-feature capability maps. Mirrors PHP Plan_Config so JS | |
| 409 | - // never has to ask "is the user Pro?" — it asks "can the user X?". | |
| 410 | - '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. | |
| 411 | 458 | // MCP (Model Context Protocol) connection details for the MCP page. |
| 412 | 459 | 'mcp' => $this->get_mcp_globals(), |
| 413 | 460 | // Google OAuth: JS only ever gets a nonce-signed admin-post URL. |
| 414 | 461 | // The consent URL, client ID, and scopes are assembled by the proxy, |
| @@ -540,13 +587,22 @@ | ||
| 540 | 587 | ]); |
| 541 | 588 | } |
| 542 | 589 | |
| 543 | 590 | /** |
| 544 | - * Render import/export page | |
| 591 | + * Render the Import / Export page (ThinkRank's own data, out and back). | |
| 545 | 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 | + * | |
| 546 | 598 | * @return void |
| 547 | 599 | */ |
| 548 | 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 | + | |
| 549 | 605 | $this->render_admin_page('import-export', [ |
| 550 | 606 | 'page_title' => __('Import / Export', 'thinkrank'), |
| 551 | 607 | ]); |
| 552 | 608 | } |
| @@ -551,19 +607,19 @@ | ||
| 551 | 607 | ]); |
| 552 | 608 | } |
| 553 | 609 | |
| 554 | 610 | /** |
| 555 | - * Render the Migration page (re-run SEO data imports). | |
| 611 | + * Render the Migration page (import SEO data from another plugin). | |
| 556 | 612 | * |
| 557 | - * Defense in depth: the submenu is only registered when the setting is on, | |
| 558 | - * 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. | |
| 559 | 614 | * |
| 560 | 615 | * @return void |
| 561 | 616 | */ |
| 562 | 617 | public function render_migration_page(): void { |
| 563 | - if (!Settings::instance()->get('enable_migration_tools', false)) { | |
| 618 | + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) { | |
| 564 | 619 | wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank')); |
| 565 | 620 | } |
| 621 | + | |
| 566 | 622 | $this->render_admin_page('migration', [ |
| 567 | 623 | 'page_title' => __('Migration', 'thinkrank'), |
| 568 | 624 | ]); |
| 569 | 625 | } |
| @@ -730,8 +786,14 @@ | ||
| 730 | 786 | */ |
| 731 | 787 | public function dismiss_notice(): void { |
| 732 | 788 | check_ajax_referer('thinkrank_admin', 'nonce'); |
| 733 | 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 | + | |
| 734 | 796 | $notice_type = sanitize_key($_POST['notice_type'] ?? ''); |
| 735 | 797 | |
| 736 | 798 | if ($notice_type === 'welcome') { |
| 737 | 799 | delete_option('thinkrank_show_welcome'); |
| @@ -759,8 +821,9 @@ | ||
| 759 | 821 | * |
| 760 | 822 | * @return string Menu icon |
| 761 | 823 | */ |
| 762 | 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. | |
| 763 | 826 | return 'data:image/svg+xml;base64,' . base64_encode( |
| 764 | 827 | '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 765 | 828 | <g clipPath="url(#thinkrank-clip)"> |
| 766 | 829 | <g filter="url(#thinkrank-shadow)"> |
| @@ -808,9 +871,9 @@ | ||
| 808 | 871 | */ |
| 809 | 872 | private function get_admin_settings(): array { |
| 810 | 873 | return [ |
| 811 | 874 | |
| 812 | - 'ai_provider' => $this->settings->get('ai_provider', 'openai'), | |
| 875 | + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE), | |
| 813 | 876 | 'cache_duration' => $this->settings->get('cache_duration', 3600), |
| 814 | 877 | ]; |
| 815 | 878 | } |
| 816 | 879 | |
| @@ -942,10 +1005,11 @@ | ||
| 942 | 1005 | 'thinkrank_page_thinkrank-ai-tools', |
| 943 | 1006 | 'thinkrank_page_thinkrank-settings', |
| 944 | 1007 | 'thinkrank_page_thinkrank-usages', |
| 945 | 1008 | 'thinkrank_page_thinkrank-license', |
| 1009 | + 'thinkrank_page_thinkrank-import-export', | |
| 946 | 1010 | 'thinkrank_page_thinkrank-migration' |
| 947 | - ] ) ) { | |
| 1011 | + ] , true) ) { | |
| 948 | 1012 | |
| 949 | 1013 | remove_all_actions( 'user_admin_notices' ); |
| 950 | 1014 | remove_all_actions( 'admin_notices' ); |
| 951 | 1015 | remove_all_actions( 'all_admin_notices' ); |