PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.0
2.12.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 All 53 releases
← All changes | includes/admin/class-manager.php +102 -34 1.28.0 → 2.12.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 }
@@ -730,8 +800,14 @@
730 800 */
731 801 public function dismiss_notice(): void {
732 802 check_ajax_referer('thinkrank_admin', 'nonce');
733 803
804 + // The nonce proves intent, not authorization — dismissing a site-wide
805 + // notice deletes an option, so require a capability as well.
806 + if (!current_user_can('edit_posts')) {
807 + wp_die(-1, 403);
808 + }
809 +
734 810 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
735 811
736 812 if ($notice_type === 'welcome') {
737 813 delete_option('thinkrank_show_welcome');
@@ -740,19 +816,14 @@
740 816 wp_die();
741 817 }
742 818
743 819 /**
744 - * Check if API key is configured
820 + * Check if the selected AI provider is configured
745 821 *
746 - * @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
747 823 */
748 824 private function has_api_key_configured(): bool {
749 - $settings = \ThinkRank\Core\Settings::instance();
750 -
751 - return !empty($settings->get('openai_api_key'))
752 - || !empty($settings->get('claude_api_key'))
753 - || !empty($settings->get('gemini_api_key'))
754 - || !empty($settings->get('openrouter_api_key'));
825 + return \ThinkRank\Core\Settings::instance()->has_ai_provider_configured();
755 826 }
756 827
757 828 /**
758 829 * Get menu icon
@@ -759,8 +830,9 @@
759 830 *
760 831 * @return string Menu icon
761 832 */
762 833 private function get_menu_icon(): string {
834 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- a data: URI for the menu icon must be base64.
763 835 return 'data:image/svg+xml;base64,' . base64_encode(
764 836 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
765 837 <g clipPath="url(#thinkrank-clip)">
766 838 <g filter="url(#thinkrank-shadow)">
@@ -808,28 +880,23 @@
808 880 */
809 881 private function get_admin_settings(): array {
810 882 return [
811 883
812 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
884 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
813 885 'cache_duration' => $this->settings->get('cache_duration', 3600),
814 886 ];
815 887 }
816 888
817 889 /**
818 - * Whether any AI provider API key is configured
890 + * Whether the selected AI provider is configured
819 891 *
820 - * Mirrors the check used by ThinkRank\AI\Manager.
892 + * Same answer as ThinkRank\AI\Manager — both read
893 + * Settings::has_ai_provider_configured().
821 894 *
822 895 * @return bool
823 896 */
824 897 private function is_ai_configured(): bool {
825 - foreach (['openai_api_key', 'claude_api_key', 'gemini_api_key', 'openrouter_api_key'] as $key) {
826 - if (!empty($this->settings->get($key, ''))) {
827 - return true;
828 - }
829 - }
830 -
831 - return false;
898 + return $this->settings->has_ai_provider_configured();
832 899 }
833 900
834 901 /**
835 902 * Get internationalization strings
@@ -942,10 +1009,11 @@
942 1009 'thinkrank_page_thinkrank-ai-tools',
943 1010 'thinkrank_page_thinkrank-settings',
944 1011 'thinkrank_page_thinkrank-usages',
945 1012 'thinkrank_page_thinkrank-license',
1013 + 'thinkrank_page_thinkrank-import-export',
946 1014 'thinkrank_page_thinkrank-migration'
947 - ] ) ) {
1015 + ] , true) ) {
948 1016
949 1017 remove_all_actions( 'user_admin_notices' );
950 1018 remove_all_actions( 'admin_notices' );
951 1019 remove_all_actions( 'all_admin_notices' );