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 +116 -34 1.25.0 → 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 */
@@ -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,12 @@
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 + // 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),
374 436 // Whether any AI provider API key is configured — used to gate
375 437 // "Generate with AI" buttons in the UI
376 438 'aiConfigured' => $this->is_ai_configured(),
377 439 // Plugin version - directly available without API call
@@ -383,8 +445,13 @@
383 445 'faviconUrl' => get_site_icon_url(64) ?: '',
384 446 'adminEmail' => get_option('admin_email'),
385 447 // Post types for Global SEO navigation
386 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(),
387 454 // Role Manager: capabilities the current user holds + the
388 455 // section → capability map, so the SPA can hide sections a role
389 456 // cannot access. Administrators receive every capability.
390 457 'caps' => Capability_Manager::user_capabilities(),
@@ -391,13 +458,10 @@
391 458 'sectionCaps' => Capability_Manager::section_map(),
392 459 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
393 460 // Pro detection flag
394 461 '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(),
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.
400 464 // MCP (Model Context Protocol) connection details for the MCP page.
401 465 'mcp' => $this->get_mcp_globals(),
402 466 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
403 467 // The consent URL, client ID, and scopes are assembled by the proxy,
@@ -408,8 +472,14 @@
408 472 // 'contract' (upgraded off the old token flow) or
409 473 // 'credentials' (stored tokens no longer decryptable).
410 474 'reconnectReason' => (string) get_option('thinkrank_google_reconnect_required', ''),
411 475 ],
476 + // Setup Wizard state — the dashboard Quick Access widget surfaces a
477 + // "Complete Setup" shortcut while onboarding is unfinished.
478 + 'setupWizard' => [
479 + 'completed' => (bool) get_option(Setup_Wizard::OPT_COMPLETED, false),
480 + 'url' => admin_url('admin.php?page=' . Setup_Wizard::PAGE_SLUG),
481 + ],
412 482 ]);
413 483
414 484 }
415 485
@@ -425,8 +495,13 @@
425 495 }
426 496
427 497 // Check for plugin updates
428 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();
429 504 }
430 505
431 506 /**
432 507 * Load admin page
@@ -523,13 +598,22 @@
523 598 ]);
524 599 }
525 600
526 601 /**
527 - * Render import/export page
602 + * Render the Import / Export page (ThinkRank's own data, out and back).
528 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 + *
529 609 * @return void
530 610 */
531 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 +
532 616 $this->render_admin_page('import-export', [
533 617 'page_title' => __('Import / Export', 'thinkrank'),
534 618 ]);
535 619 }
@@ -534,19 +618,19 @@
534 618 ]);
535 619 }
536 620
537 621 /**
538 - * Render the Migration page (re-run SEO data imports).
622 + * Render the Migration page (import SEO data from another plugin).
539 623 *
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.
624 + * Same defense in depth as above, against its own setting.
542 625 *
543 626 * @return void
544 627 */
545 628 public function render_migration_page(): void {
546 - if (!Settings::instance()->get('enable_migration_tools', false)) {
629 + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) {
547 630 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
548 631 }
632 +
549 633 $this->render_admin_page('migration', [
550 634 'page_title' => __('Migration', 'thinkrank'),
551 635 ]);
552 636 }
@@ -713,8 +797,14 @@
713 797 */
714 798 public function dismiss_notice(): void {
715 799 check_ajax_referer('thinkrank_admin', 'nonce');
716 800
801 + // The nonce proves intent, not authorization — dismissing a site-wide
802 + // notice deletes an option, so require a capability as well.
803 + if (!current_user_can('edit_posts')) {
804 + wp_die(-1, 403);
805 + }
806 +
717 807 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
718 808
719 809 if ($notice_type === 'welcome') {
720 810 delete_option('thinkrank_show_welcome');
@@ -723,19 +813,14 @@
723 813 wp_die();
724 814 }
725 815
726 816 /**
727 - * Check if API key is configured
817 + * Check if the selected AI provider is configured
728 818 *
729 - * @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
730 820 */
731 821 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'));
822 + return \ThinkRank\Core\Settings::instance()->has_ai_provider_configured();
738 823 }
739 824
740 825 /**
741 826 * Get menu icon
@@ -742,8 +827,9 @@
742 827 *
743 828 * @return string Menu icon
744 829 */
745 830 private function get_menu_icon(): string {
831 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- a data: URI for the menu icon must be base64.
746 832 return 'data:image/svg+xml;base64,' . base64_encode(
747 833 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
748 834 <g clipPath="url(#thinkrank-clip)">
749 835 <g filter="url(#thinkrank-shadow)">
@@ -791,28 +877,23 @@
791 877 */
792 878 private function get_admin_settings(): array {
793 879 return [
794 880
795 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
881 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
796 882 'cache_duration' => $this->settings->get('cache_duration', 3600),
797 883 ];
798 884 }
799 885
800 886 /**
801 - * Whether any AI provider API key is configured
887 + * Whether the selected AI provider is configured
802 888 *
803 - * Mirrors the check used by ThinkRank\AI\Manager.
889 + * Same answer as ThinkRank\AI\Manager — both read
890 + * Settings::has_ai_provider_configured().
804 891 *
805 892 * @return bool
806 893 */
807 894 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;
895 + return $this->settings->has_ai_provider_configured();
815 896 }
816 897
817 898 /**
818 899 * Get internationalization strings
@@ -925,10 +1006,11 @@
925 1006 'thinkrank_page_thinkrank-ai-tools',
926 1007 'thinkrank_page_thinkrank-settings',
927 1008 'thinkrank_page_thinkrank-usages',
928 1009 'thinkrank_page_thinkrank-license',
1010 + 'thinkrank_page_thinkrank-import-export',
929 1011 'thinkrank_page_thinkrank-migration'
930 - ] ) ) {
1012 + ] , true) ) {
931 1013
932 1014 remove_all_actions( 'user_admin_notices' );
933 1015 remove_all_actions( 'admin_notices' );
934 1016 remove_all_actions( 'all_admin_notices' );