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 +119 -34 1.26.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 */
@@ -94,8 +111,15 @@
94 111 */
95 112 private Focus_Keyword_Ajax $focus_keyword_ajax;
96 113
97 114 /**
115 + * SEO Quick Edit modal AJAX handler instance
116 + *
117 + * @var Seo_Quick_Edit_Ajax
118 + */
119 + private Seo_Quick_Edit_Ajax $seo_quick_edit_ajax;
120 +
121 + /**
98 122 * Bulk action manager instance
99 123 *
100 124 * @var Bulk_Action_Manager
101 125 */
@@ -127,10 +151,13 @@
127 151 $this->metabox_manager = new Metabox_Manager($this->settings);
128 152 $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager);
129 153 $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager);
130 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);
131 157 $this->post_list_columns = new Post_List_Columns();
132 158 $this->focus_keyword_ajax = new Focus_Keyword_Ajax();
159 + $this->seo_quick_edit_ajax = new Seo_Quick_Edit_Ajax();
133 160 $this->bulk_action_manager = new Bulk_Action_Manager();
134 161 $this->post_list_filters = new Post_List_Filters();
135 162 }
136 163
@@ -163,8 +190,16 @@
163 190 // Initialize Divi Visual Builder integration (hooks gate on the VB
164 191 // request, so they no-op without Divi)
165 192 $this->divi_metabox->init();
166 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 +
167 202 // Initialize post list columns
168 203 $this->post_list_columns->init();
169 204
170 205 // Initialize focus keyword AJAX handler
@@ -169,8 +204,11 @@
169 204
170 205 // Initialize focus keyword AJAX handler
171 206 $this->focus_keyword_ajax->init();
172 207
208 + // Initialize SEO Quick Edit modal AJAX handler
209 + $this->seo_quick_edit_ajax->init();
210 +
173 211 // Initialize Bulk Action Manager
174 212 $this->bulk_action_manager->init();
175 213
176 214 // Initialize Post List Filters
@@ -249,13 +287,33 @@
249 287 'thinkrank-settings',
250 288 [$this, 'render_settings_page']
251 289 );
252 290
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)) {
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)) {
258 316 $this->pages['migration'] = add_submenu_page(
259 317 'thinkrank',
260 318 __('Migration', 'thinkrank'),
261 319 __('Migration', 'thinkrank'),
@@ -370,8 +428,15 @@
370 428 'capabilities' => $this->get_user_capabilities(),
371 429 'settings' => $this->get_admin_settings(),
372 430 'i18n' => $this->get_i18n_strings(),
373 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),
374 439 // Whether any AI provider API key is configured — used to gate
375 440 // "Generate with AI" buttons in the UI
376 441 'aiConfigured' => $this->is_ai_configured(),
377 442 // Plugin version - directly available without API call
@@ -383,8 +448,13 @@
383 448 'faviconUrl' => get_site_icon_url(64) ?: '',
384 449 'adminEmail' => get_option('admin_email'),
385 450 // Post types for Global SEO navigation
386 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(),
387 457 // Role Manager: capabilities the current user holds + the
388 458 // section → capability map, so the SPA can hide sections a role
389 459 // cannot access. Administrators receive every capability.
390 460 'caps' => Capability_Manager::user_capabilities(),
@@ -391,13 +461,10 @@
391 461 'sectionCaps' => Capability_Manager::section_map(),
392 462 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
393 463 // Pro detection flag
394 464 '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(),
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.
400 467 // MCP (Model Context Protocol) connection details for the MCP page.
401 468 'mcp' => $this->get_mcp_globals(),
402 469 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
403 470 // The consent URL, client ID, and scopes are assembled by the proxy,
@@ -408,8 +475,14 @@
408 475 // 'contract' (upgraded off the old token flow) or
409 476 // 'credentials' (stored tokens no longer decryptable).
410 477 'reconnectReason' => (string) get_option('thinkrank_google_reconnect_required', ''),
411 478 ],
479 + // Setup Wizard state — the dashboard Quick Access widget surfaces a
480 + // "Complete Setup" shortcut while onboarding is unfinished.
481 + 'setupWizard' => [
482 + 'completed' => (bool) get_option(Setup_Wizard::OPT_COMPLETED, false),
483 + 'url' => admin_url('admin.php?page=' . Setup_Wizard::PAGE_SLUG),
484 + ],
412 485 ]);
413 486
414 487 }
415 488
@@ -425,8 +498,13 @@
425 498 }
426 499
427 500 // Check for plugin updates
428 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();
429 507 }
430 508
431 509 /**
432 510 * Load admin page
@@ -523,13 +601,22 @@
523 601 ]);
524 602 }
525 603
526 604 /**
527 - * Render import/export page
605 + * Render the Import / Export page (ThinkRank's own data, out and back).
528 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 + *
529 612 * @return void
530 613 */
531 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 +
532 619 $this->render_admin_page('import-export', [
533 620 'page_title' => __('Import / Export', 'thinkrank'),
534 621 ]);
535 622 }
@@ -534,19 +621,19 @@
534 621 ]);
535 622 }
536 623
537 624 /**
538 - * Render the Migration page (re-run SEO data imports).
625 + * Render the Migration page (import SEO data from another plugin).
539 626 *
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.
627 + * Same defense in depth as above, against its own setting.
542 628 *
543 629 * @return void
544 630 */
545 631 public function render_migration_page(): void {
546 - if (!Settings::instance()->get('enable_migration_tools', false)) {
632 + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) {
547 633 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
548 634 }
635 +
549 636 $this->render_admin_page('migration', [
550 637 'page_title' => __('Migration', 'thinkrank'),
551 638 ]);
552 639 }
@@ -713,8 +800,14 @@
713 800 */
714 801 public function dismiss_notice(): void {
715 802 check_ajax_referer('thinkrank_admin', 'nonce');
716 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 +
717 810 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
718 811
719 812 if ($notice_type === 'welcome') {
720 813 delete_option('thinkrank_show_welcome');
@@ -723,19 +816,14 @@
723 816 wp_die();
724 817 }
725 818
726 819 /**
727 - * Check if API key is configured
820 + * Check if the selected AI provider is configured
728 821 *
729 - * @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
730 823 */
731 824 private function has_api_key_configured(): bool {
732 - $settings = \ThinkRank\Core\Settings::instance();
733 -
734 - return !empty($settings->get('openai_api_key'))
735 - || !empty($settings->get('claude_api_key'))
736 - || !empty($settings->get('gemini_api_key'))
737 - || !empty($settings->get('openrouter_api_key'));
825 + return \ThinkRank\Core\Settings::instance()->has_ai_provider_configured();
738 826 }
739 827
740 828 /**
741 829 * Get menu icon
@@ -742,8 +830,9 @@
742 830 *
743 831 * @return string Menu icon
744 832 */
745 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.
746 835 return 'data:image/svg+xml;base64,' . base64_encode(
747 836 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
748 837 <g clipPath="url(#thinkrank-clip)">
749 838 <g filter="url(#thinkrank-shadow)">
@@ -791,28 +880,23 @@
791 880 */
792 881 private function get_admin_settings(): array {
793 882 return [
794 883
795 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
884 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
796 885 'cache_duration' => $this->settings->get('cache_duration', 3600),
797 886 ];
798 887 }
799 888
800 889 /**
801 - * Whether any AI provider API key is configured
890 + * Whether the selected AI provider is configured
802 891 *
803 - * Mirrors the check used by ThinkRank\AI\Manager.
892 + * Same answer as ThinkRank\AI\Manager — both read
893 + * Settings::has_ai_provider_configured().
804 894 *
805 895 * @return bool
806 896 */
807 897 private function is_ai_configured(): bool {
808 - foreach (['openai_api_key', 'claude_api_key', 'gemini_api_key', 'openrouter_api_key'] as $key) {
809 - if (!empty($this->settings->get($key, ''))) {
810 - return true;
811 - }
812 - }
813 -
814 - return false;
898 + return $this->settings->has_ai_provider_configured();
815 899 }
816 900
817 901 /**
818 902 * Get internationalization strings
@@ -925,10 +1009,11 @@
925 1009 'thinkrank_page_thinkrank-ai-tools',
926 1010 'thinkrank_page_thinkrank-settings',
927 1011 'thinkrank_page_thinkrank-usages',
928 1012 'thinkrank_page_thinkrank-license',
1013 + 'thinkrank_page_thinkrank-import-export',
929 1014 'thinkrank_page_thinkrank-migration'
930 - ] ) ) {
1015 + ] , true) ) {
931 1016
932 1017 remove_all_actions( 'user_admin_notices' );
933 1018 remove_all_actions( 'admin_notices' );
934 1019 remove_all_actions( 'all_admin_notices' );