PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.11.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.11.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 All 52 releases
← All changes | includes/admin/class-manager.php +94 -33 1.29.0 → 2.11.0 View file →
@@ -16,12 +16,15 @@
16 16 use ThinkRank\Core\Settings;
17 17 use ThinkRank\Core\Database;
18 18 use ThinkRank\Core\Plan_Config;
19 19 use ThinkRank\Core\Capability_Manager;
20 +use ThinkRank\Config\Local_Business_Types_Config;
20 21 use ThinkRank\Admin\Metabox_Manager;
21 22 use ThinkRank\Admin\Elementor_Metabox;
22 23 use ThinkRank\Admin\Oxygen_Metabox;
23 24 use ThinkRank\Admin\Divi_Metabox;
25 +use ThinkRank\Admin\Bricks_Metabox;
26 +use ThinkRank\Admin\Beaver_Metabox;
24 27 use ThinkRank\Admin\Bulk_Action_Manager;
25 28 use ThinkRank\Admin\Post_List_Filters;
26 29
27 30 // Prevent direct access
@@ -80,8 +83,22 @@
80 83 */
81 84 private Divi_Metabox $divi_metabox;
82 85
83 86 /**
87 + * Bricks builder metabox integration instance
88 + *
89 + * @var Bricks_Metabox
90 + */
91 + private Bricks_Metabox $bricks_metabox;
92 +
93 + /**
94 + * Beaver Builder metabox integration
95 + *
96 + * @var Beaver_Metabox
97 + */
98 + private Beaver_Metabox $beaver_metabox;
99 +
100 + /**
84 101 * Post list columns instance
85 102 *
86 103 * @var Post_List_Columns
87 104 */
@@ -134,8 +151,10 @@
134 151 $this->metabox_manager = new Metabox_Manager($this->settings);
135 152 $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager);
136 153 $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager);
137 154 $this->divi_metabox = new Divi_Metabox($this->metabox_manager);
155 + $this->bricks_metabox = new Bricks_Metabox($this->metabox_manager);
156 + $this->beaver_metabox = new Beaver_Metabox($this->metabox_manager);
138 157 $this->post_list_columns = new Post_List_Columns();
139 158 $this->focus_keyword_ajax = new Focus_Keyword_Ajax();
140 159 $this->seo_quick_edit_ajax = new Seo_Quick_Edit_Ajax();
141 160 $this->bulk_action_manager = new Bulk_Action_Manager();
@@ -171,8 +190,16 @@
171 190 // Initialize Divi Visual Builder integration (hooks gate on the VB
172 191 // request, so they no-op without Divi)
173 192 $this->divi_metabox->init();
174 193
194 + // Initialize Bricks builder integration (hooks gate on Bricks' own
195 + // builder detector, so they no-op without Bricks)
196 + $this->bricks_metabox->init();
197 +
198 + // Initialize Beaver Builder integration (hooks gate on Beaver Builder's
199 + // own builder detector, so they no-op without Beaver Builder)
200 + $this->beaver_metabox->init();
201 +
175 202 // Initialize post list columns
176 203 $this->post_list_columns->init();
177 204
178 205 // Initialize focus keyword AJAX handler
@@ -260,13 +287,33 @@
260 287 'thinkrank-settings',
261 288 [$this, 'render_settings_page']
262 289 );
263 290
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)) {
291 + // Two separate screens, one per setting, so each menu item appears
292 + // exactly when its own feature is on. They shared a page (and therefore
293 + // a menu item) until #228: with one route, turning Import / Export off
294 + // still left "Import / Export" in the sidebar whenever Migration Tools
295 + // happened to be on, which is the opposite of what the switch promises.
296 + //
297 + // Capability matches the import/export REST endpoints (`manage_options`)
298 + // so the UI and API stay in agreement.
299 +
300 + // ThinkRank's own data, out to a file and back. Off by default.
301 + if ((bool) Settings::instance()->get('enable_import_export', false)) {
302 + $this->pages['import-export'] = add_submenu_page(
303 + 'thinkrank',
304 + __('Import / Export', 'thinkrank'),
305 + __('Import / Export', 'thinkrank'),
306 + 'manage_options',
307 + 'thinkrank-import-export',
308 + [$this, 'render_import_export_page']
309 + );
310 + }
311 +
312 + // Importing FROM another SEO plugin. Off by default. Slug kept as
313 + // thinkrank-migration so existing links, bookmarks and the docs keep
314 + // resolving to the migration screen they always meant.
315 + if ((bool) Settings::instance()->get('enable_migration_tools', false)) {
269 316 $this->pages['migration'] = add_submenu_page(
270 317 'thinkrank',
271 318 __('Migration', 'thinkrank'),
272 319 __('Migration', 'thinkrank'),
@@ -381,8 +428,15 @@
381 428 'capabilities' => $this->get_user_capabilities(),
382 429 'settings' => $this->get_admin_settings(),
383 430 'i18n' => $this->get_i18n_strings(),
384 431 'isAdmin' => current_user_can('manage_options'),
432 + // Simple / Advanced navigation for this user (#730). Read once
433 + // when the page is built; the header switch updates it in place.
434 + 'uiMode' => UI_Mode::get(),
435 + // One flag per half of the Import / Export page: the "import from
436 + // another SEO plugin" section, and ThinkRank's own export/restore.
437 + 'migrationToolsEnabled' => (bool) $this->settings->get('enable_migration_tools', false),
438 + 'importExportEnabled' => (bool) $this->settings->get('enable_import_export', false),
385 439 // Whether any AI provider API key is configured — used to gate
386 440 // "Generate with AI" buttons in the UI
387 441 'aiConfigured' => $this->is_ai_configured(),
388 442 // Plugin version - directly available without API call
@@ -394,8 +448,13 @@
394 448 'faviconUrl' => get_site_icon_url(64) ?: '',
395 449 'adminEmail' => get_option('admin_email'),
396 450 // Post types for Global SEO navigation
397 451 'postTypes' => $this->get_public_post_types(),
452 + // schema.org LocalBusiness subtypes for the Local SEO control.
453 + // Localized rather than mirrored in a JS config: the list runs to
454 + // ~150 entries and is also the MCP ability's enum, so a second copy
455 + // would be a second thing to keep in step (#623).
456 + 'localBusinessTypes' => Local_Business_Types_Config::get_options(),
398 457 // Role Manager: capabilities the current user holds + the
399 458 // section → capability map, so the SPA can hide sections a role
400 459 // cannot access. Administrators receive every capability.
401 460 'caps' => Capability_Manager::user_capabilities(),
@@ -402,13 +461,10 @@
402 461 'sectionCaps' => Capability_Manager::section_map(),
403 462 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
404 463 // Pro detection flag
405 464 '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(),
465 + // NB: no per-feature capability maps here; each screen reads its
466 + // own state over REST, so a localized copy cannot drift from it.
411 467 // MCP (Model Context Protocol) connection details for the MCP page.
412 468 'mcp' => $this->get_mcp_globals(),
413 469 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
414 470 // The consent URL, client ID, and scopes are assembled by the proxy,
@@ -442,8 +498,13 @@
442 498 }
443 499
444 500 // Check for plugin updates
445 501 $this->check_plugin_updates();
502 +
503 + // One-time: a Key Features value saved while commas were delimiters
504 + // becomes one feature per line, so the next regeneration publishes the
505 + // bullets the site already had rather than one merged line.
506 + \ThinkRank\SEO\LLMs_Txt_Manager::maybe_migrate_legacy_key_features();
446 507 }
447 508
448 509 /**
449 510 * Load admin page
@@ -540,13 +601,22 @@
540 601 ]);
541 602 }
542 603
543 604 /**
544 - * Render import/export page
605 + * Render the Import / Export page (ThinkRank's own data, out and back).
545 606 *
607 + * Defense in depth: the submenu is only registered while the setting is on,
608 + * but re-check here so a direct hit on the page URL cannot bypass the gate.
609 + * The export REST routes carry their own `manage_options` check, so nothing
610 + * is protected by the page alone.
611 + *
546 612 * @return void
547 613 */
548 614 public function render_import_export_page(): void {
615 + if (!(bool) Settings::instance()->get('enable_import_export', false)) {
616 + wp_die(esc_html__('Import / Export is not enabled.', 'thinkrank'));
617 + }
618 +
549 619 $this->render_admin_page('import-export', [
550 620 'page_title' => __('Import / Export', 'thinkrank'),
551 621 ]);
552 622 }
@@ -551,19 +621,19 @@
551 621 ]);
552 622 }
553 623
554 624 /**
555 - * Render the Migration page (re-run SEO data imports).
625 + * Render the Migration page (import SEO data from another plugin).
556 626 *
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.
627 + * Same defense in depth as above, against its own setting.
559 628 *
560 629 * @return void
561 630 */
562 631 public function render_migration_page(): void {
563 - if (!Settings::instance()->get('enable_migration_tools', false)) {
632 + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) {
564 633 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
565 634 }
635 +
566 636 $this->render_admin_page('migration', [
567 637 'page_title' => __('Migration', 'thinkrank'),
568 638 ]);
569 639 }
@@ -746,19 +816,14 @@
746 816 wp_die();
747 817 }
748 818
749 819 /**
750 - * Check if API key is configured
820 + * Check if the selected AI provider is configured
751 821 *
752 - * @return bool True if at least one API key is configured
822 + * @return bool True when the chosen provider has what it needs to run
753 823 */
754 824 private function has_api_key_configured(): bool {
755 - $settings = \ThinkRank\Core\Settings::instance();
756 -
757 - return !empty($settings->get('openai_api_key'))
758 - || !empty($settings->get('claude_api_key'))
759 - || !empty($settings->get('gemini_api_key'))
760 - || !empty($settings->get('openrouter_api_key'));
825 + return \ThinkRank\Core\Settings::instance()->has_ai_provider_configured();
761 826 }
762 827
763 828 /**
764 829 * Get menu icon
@@ -815,28 +880,23 @@
815 880 */
816 881 private function get_admin_settings(): array {
817 882 return [
818 883
819 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
884 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
820 885 'cache_duration' => $this->settings->get('cache_duration', 3600),
821 886 ];
822 887 }
823 888
824 889 /**
825 - * Whether any AI provider API key is configured
890 + * Whether the selected AI provider is configured
826 891 *
827 - * Mirrors the check used by ThinkRank\AI\Manager.
892 + * Same answer as ThinkRank\AI\Manager — both read
893 + * Settings::has_ai_provider_configured().
828 894 *
829 895 * @return bool
830 896 */
831 897 private function is_ai_configured(): bool {
832 - foreach (['openai_api_key', 'claude_api_key', 'gemini_api_key', 'openrouter_api_key'] as $key) {
833 - if (!empty($this->settings->get($key, ''))) {
834 - return true;
835 - }
836 - }
837 -
838 - return false;
898 + return $this->settings->has_ai_provider_configured();
839 899 }
840 900
841 901 /**
842 902 * Get internationalization strings
@@ -949,8 +1009,9 @@
949 1009 'thinkrank_page_thinkrank-ai-tools',
950 1010 'thinkrank_page_thinkrank-settings',
951 1011 'thinkrank_page_thinkrank-usages',
952 1012 'thinkrank_page_thinkrank-license',
1013 + 'thinkrank_page_thinkrank-import-export',
953 1014 'thinkrank_page_thinkrank-migration'
954 1015 ] , true) ) {
955 1016
956 1017 remove_all_actions( 'user_admin_notices' );