PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.0.1 All 51 releases
← All changes | includes/admin/class-manager.php +91 -33 2.1.1 → 2.10.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,12 @@
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 + // One flag per half of the Import / Export page: the "import from
433 + // another SEO plugin" section, and ThinkRank's own export/restore.
434 + 'migrationToolsEnabled' => (bool) $this->settings->get('enable_migration_tools', false),
435 + 'importExportEnabled' => (bool) $this->settings->get('enable_import_export', false),
385 436 // Whether any AI provider API key is configured — used to gate
386 437 // "Generate with AI" buttons in the UI
387 438 'aiConfigured' => $this->is_ai_configured(),
388 439 // Plugin version - directly available without API call
@@ -394,8 +445,13 @@
394 445 'faviconUrl' => get_site_icon_url(64) ?: '',
395 446 'adminEmail' => get_option('admin_email'),
396 447 // Post types for Global SEO navigation
397 448 'postTypes' => $this->get_public_post_types(),
449 + // schema.org LocalBusiness subtypes for the Local SEO control.
450 + // Localized rather than mirrored in a JS config: the list runs to
451 + // ~150 entries and is also the MCP ability's enum, so a second copy
452 + // would be a second thing to keep in step (#623).
453 + 'localBusinessTypes' => Local_Business_Types_Config::get_options(),
398 454 // Role Manager: capabilities the current user holds + the
399 455 // section → capability map, so the SPA can hide sections a role
400 456 // cannot access. Administrators receive every capability.
401 457 'caps' => Capability_Manager::user_capabilities(),
@@ -402,13 +458,10 @@
402 458 'sectionCaps' => Capability_Manager::section_map(),
403 459 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
404 460 // Pro detection flag
405 461 'isPro' => Plan_Config::is_pro(),
406 - // Data update frequency (Pro: daily, Free: every 3 days)
407 - 'dataUpdateFrequency' => Plan_Config::is_pro() ? 'daily' : '3days',
408 - // NB: no per-feature capability maps here. Email Reporting reads
409 - // its capabilities from GET /thinkrank/v1/email-report/config;
410 - // localizing a second copy only invited the two to drift.
462 + // NB: no per-feature capability maps here; each screen reads its
463 + // own state over REST, so a localized copy cannot drift from it.
411 464 // MCP (Model Context Protocol) connection details for the MCP page.
412 465 'mcp' => $this->get_mcp_globals(),
413 466 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
414 467 // The consent URL, client ID, and scopes are assembled by the proxy,
@@ -442,8 +495,13 @@
442 495 }
443 496
444 497 // Check for plugin updates
445 498 $this->check_plugin_updates();
499 +
500 + // One-time: a Key Features value saved while commas were delimiters
501 + // becomes one feature per line, so the next regeneration publishes the
502 + // bullets the site already had rather than one merged line.
503 + \ThinkRank\SEO\LLMs_Txt_Manager::maybe_migrate_legacy_key_features();
446 504 }
447 505
448 506 /**
449 507 * Load admin page
@@ -540,13 +598,22 @@
540 598 ]);
541 599 }
542 600
543 601 /**
544 - * Render import/export page
602 + * Render the Import / Export page (ThinkRank's own data, out and back).
545 603 *
604 + * Defense in depth: the submenu is only registered while the setting is on,
605 + * but re-check here so a direct hit on the page URL cannot bypass the gate.
606 + * The export REST routes carry their own `manage_options` check, so nothing
607 + * is protected by the page alone.
608 + *
546 609 * @return void
547 610 */
548 611 public function render_import_export_page(): void {
612 + if (!(bool) Settings::instance()->get('enable_import_export', false)) {
613 + wp_die(esc_html__('Import / Export is not enabled.', 'thinkrank'));
614 + }
615 +
549 616 $this->render_admin_page('import-export', [
550 617 'page_title' => __('Import / Export', 'thinkrank'),
551 618 ]);
552 619 }
@@ -551,19 +618,19 @@
551 618 ]);
552 619 }
553 620
554 621 /**
555 - * Render the Migration page (re-run SEO data imports).
622 + * Render the Migration page (import SEO data from another plugin).
556 623 *
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.
624 + * Same defense in depth as above, against its own setting.
559 625 *
560 626 * @return void
561 627 */
562 628 public function render_migration_page(): void {
563 - if (!Settings::instance()->get('enable_migration_tools', false)) {
629 + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) {
564 630 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
565 631 }
632 +
566 633 $this->render_admin_page('migration', [
567 634 'page_title' => __('Migration', 'thinkrank'),
568 635 ]);
569 636 }
@@ -746,19 +813,14 @@
746 813 wp_die();
747 814 }
748 815
749 816 /**
750 - * Check if API key is configured
817 + * Check if the selected AI provider is configured
751 818 *
752 - * @return bool True if at least one API key is configured
819 + * @return bool True when the chosen provider has what it needs to run
753 820 */
754 821 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'));
822 + return \ThinkRank\Core\Settings::instance()->has_ai_provider_configured();
761 823 }
762 824
763 825 /**
764 826 * Get menu icon
@@ -815,28 +877,23 @@
815 877 */
816 878 private function get_admin_settings(): array {
817 879 return [
818 880
819 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
881 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
820 882 'cache_duration' => $this->settings->get('cache_duration', 3600),
821 883 ];
822 884 }
823 885
824 886 /**
825 - * Whether any AI provider API key is configured
887 + * Whether the selected AI provider is configured
826 888 *
827 - * Mirrors the check used by ThinkRank\AI\Manager.
889 + * Same answer as ThinkRank\AI\Manager — both read
890 + * Settings::has_ai_provider_configured().
828 891 *
829 892 * @return bool
830 893 */
831 894 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;
895 + return $this->settings->has_ai_provider_configured();
839 896 }
840 897
841 898 /**
842 899 * Get internationalization strings
@@ -949,8 +1006,9 @@
949 1006 'thinkrank_page_thinkrank-ai-tools',
950 1007 'thinkrank_page_thinkrank-settings',
951 1008 'thinkrank_page_thinkrank-usages',
952 1009 'thinkrank_page_thinkrank-license',
1010 + 'thinkrank_page_thinkrank-import-export',
953 1011 'thinkrank_page_thinkrank-migration'
954 1012 ] , true) ) {
955 1013
956 1014 remove_all_actions( 'user_admin_notices' );