PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/admin/class-manager.php +98 -17 1.27.02.7.0 View file →
@@ -20,8 +20,10 @@
20 20 use ThinkRank\Admin\Metabox_Manager;
21 21 use ThinkRank\Admin\Elementor_Metabox;
22 22 use ThinkRank\Admin\Oxygen_Metabox;
23 23 use ThinkRank\Admin\Divi_Metabox;
24 +use ThinkRank\Admin\Bricks_Metabox;
25 +use ThinkRank\Admin\Beaver_Metabox;
24 26 use ThinkRank\Admin\Bulk_Action_Manager;
25 27 use ThinkRank\Admin\Post_List_Filters;
26 28
27 29 // Prevent direct access
@@ -80,8 +82,22 @@
80 82 */
81 83 private Divi_Metabox $divi_metabox;
82 84
83 85 /**
86 + * Bricks builder metabox integration instance
87 + *
88 + * @var Bricks_Metabox
89 + */
90 + private Bricks_Metabox $bricks_metabox;
91 +
92 + /**
93 + * Beaver Builder metabox integration
94 + *
95 + * @var Beaver_Metabox
96 + */
97 + private Beaver_Metabox $beaver_metabox;
98 +
99 + /**
84 100 * Post list columns instance
85 101 *
86 102 * @var Post_List_Columns
87 103 */
@@ -94,8 +110,15 @@
94 110 */
95 111 private Focus_Keyword_Ajax $focus_keyword_ajax;
96 112
97 113 /**
114 + * SEO Quick Edit modal AJAX handler instance
115 + *
116 + * @var Seo_Quick_Edit_Ajax
117 + */
118 + private Seo_Quick_Edit_Ajax $seo_quick_edit_ajax;
119 +
120 + /**
98 121 * Bulk action manager instance
99 122 *
100 123 * @var Bulk_Action_Manager
101 124 */
@@ -127,10 +150,13 @@
127 150 $this->metabox_manager = new Metabox_Manager($this->settings);
128 151 $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager);
129 152 $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager);
130 153 $this->divi_metabox = new Divi_Metabox($this->metabox_manager);
154 + $this->bricks_metabox = new Bricks_Metabox($this->metabox_manager);
155 + $this->beaver_metabox = new Beaver_Metabox($this->metabox_manager);
131 156 $this->post_list_columns = new Post_List_Columns();
132 157 $this->focus_keyword_ajax = new Focus_Keyword_Ajax();
158 + $this->seo_quick_edit_ajax = new Seo_Quick_Edit_Ajax();
133 159 $this->bulk_action_manager = new Bulk_Action_Manager();
134 160 $this->post_list_filters = new Post_List_Filters();
135 161 }
136 162
@@ -163,8 +189,16 @@
163 189 // Initialize Divi Visual Builder integration (hooks gate on the VB
164 190 // request, so they no-op without Divi)
165 191 $this->divi_metabox->init();
166 192
193 + // Initialize Bricks builder integration (hooks gate on Bricks' own
194 + // builder detector, so they no-op without Bricks)
195 + $this->bricks_metabox->init();
196 +
197 + // Initialize Beaver Builder integration (hooks gate on Beaver Builder's
198 + // own builder detector, so they no-op without Beaver Builder)
199 + $this->beaver_metabox->init();
200 +
167 201 // Initialize post list columns
168 202 $this->post_list_columns->init();
169 203
170 204 // Initialize focus keyword AJAX handler
@@ -169,8 +203,11 @@
169 203
170 204 // Initialize focus keyword AJAX handler
171 205 $this->focus_keyword_ajax->init();
172 206
207 + // Initialize SEO Quick Edit modal AJAX handler
208 + $this->seo_quick_edit_ajax->init();
209 +
173 210 // Initialize Bulk Action Manager
174 211 $this->bulk_action_manager->init();
175 212
176 213 // Initialize Post List Filters
@@ -249,13 +286,33 @@
249 286 'thinkrank-settings',
250 287 [$this, 'render_settings_page']
251 288 );
252 289
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)) {
290 + // Two separate screens, one per setting, so each menu item appears
291 + // exactly when its own feature is on. They shared a page (and therefore
292 + // a menu item) until #228: with one route, turning Import / Export off
293 + // still left "Import / Export" in the sidebar whenever Migration Tools
294 + // happened to be on, which is the opposite of what the switch promises.
295 + //
296 + // Capability matches the import/export REST endpoints (`manage_options`)
297 + // so the UI and API stay in agreement.
298 +
299 + // ThinkRank's own data, out to a file and back. Off by default.
300 + if ((bool) Settings::instance()->get('enable_import_export', false)) {
301 + $this->pages['import-export'] = add_submenu_page(
302 + 'thinkrank',
303 + __('Import / Export', 'thinkrank'),
304 + __('Import / Export', 'thinkrank'),
305 + 'manage_options',
306 + 'thinkrank-import-export',
307 + [$this, 'render_import_export_page']
308 + );
309 + }
310 +
311 + // Importing FROM another SEO plugin. Off by default. Slug kept as
312 + // thinkrank-migration so existing links, bookmarks and the docs keep
313 + // resolving to the migration screen they always meant.
314 + if ((bool) Settings::instance()->get('enable_migration_tools', false)) {
258 315 $this->pages['migration'] = add_submenu_page(
259 316 'thinkrank',
260 317 __('Migration', 'thinkrank'),
261 318 __('Migration', 'thinkrank'),
@@ -370,8 +427,12 @@
370 427 'capabilities' => $this->get_user_capabilities(),
371 428 'settings' => $this->get_admin_settings(),
372 429 'i18n' => $this->get_i18n_strings(),
373 430 'isAdmin' => current_user_can('manage_options'),
431 + // One flag per half of the Import / Export page: the "import from
432 + // another SEO plugin" section, and ThinkRank's own export/restore.
433 + 'migrationToolsEnabled' => (bool) $this->settings->get('enable_migration_tools', false),
434 + 'importExportEnabled' => (bool) $this->settings->get('enable_import_export', false),
374 435 // Whether any AI provider API key is configured — used to gate
375 436 // "Generate with AI" buttons in the UI
376 437 'aiConfigured' => $this->is_ai_configured(),
377 438 // Plugin version - directly available without API call
@@ -391,13 +452,10 @@
391 452 'sectionCaps' => Capability_Manager::section_map(),
392 453 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
393 454 // Pro detection flag
394 455 '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(),
456 + // NB: no per-feature capability maps here; each screen reads its
457 + // own state over REST, so a localized copy cannot drift from it.
400 458 // MCP (Model Context Protocol) connection details for the MCP page.
401 459 'mcp' => $this->get_mcp_globals(),
402 460 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
403 461 // The consent URL, client ID, and scopes are assembled by the proxy,
@@ -408,8 +466,14 @@
408 466 // 'contract' (upgraded off the old token flow) or
409 467 // 'credentials' (stored tokens no longer decryptable).
410 468 'reconnectReason' => (string) get_option('thinkrank_google_reconnect_required', ''),
411 469 ],
470 + // Setup Wizard state — the dashboard Quick Access widget surfaces a
471 + // "Complete Setup" shortcut while onboarding is unfinished.
472 + 'setupWizard' => [
473 + 'completed' => (bool) get_option(Setup_Wizard::OPT_COMPLETED, false),
474 + 'url' => admin_url('admin.php?page=' . Setup_Wizard::PAGE_SLUG),
475 + ],
412 476 ]);
413 477
414 478 }
415 479
@@ -523,13 +587,22 @@
523 587 ]);
524 588 }
525 589
526 590 /**
527 - * Render import/export page
591 + * Render the Import / Export page (ThinkRank's own data, out and back).
528 592 *
593 + * Defense in depth: the submenu is only registered while the setting is on,
594 + * but re-check here so a direct hit on the page URL cannot bypass the gate.
595 + * The export REST routes carry their own `manage_options` check, so nothing
596 + * is protected by the page alone.
597 + *
529 598 * @return void
530 599 */
531 600 public function render_import_export_page(): void {
601 + if (!(bool) Settings::instance()->get('enable_import_export', false)) {
602 + wp_die(esc_html__('Import / Export is not enabled.', 'thinkrank'));
603 + }
604 +
532 605 $this->render_admin_page('import-export', [
533 606 'page_title' => __('Import / Export', 'thinkrank'),
534 607 ]);
535 608 }
@@ -534,19 +607,19 @@
534 607 ]);
535 608 }
536 609
537 610 /**
538 - * Render the Migration page (re-run SEO data imports).
611 + * Render the Migration page (import SEO data from another plugin).
539 612 *
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.
613 + * Same defense in depth as above, against its own setting.
542 614 *
543 615 * @return void
544 616 */
545 617 public function render_migration_page(): void {
546 - if (!Settings::instance()->get('enable_migration_tools', false)) {
618 + if (!(bool) Settings::instance()->get('enable_migration_tools', false)) {
547 619 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
548 620 }
621 +
549 622 $this->render_admin_page('migration', [
550 623 'page_title' => __('Migration', 'thinkrank'),
551 624 ]);
552 625 }
@@ -713,8 +786,14 @@
713 786 */
714 787 public function dismiss_notice(): void {
715 788 check_ajax_referer('thinkrank_admin', 'nonce');
716 789
790 + // The nonce proves intent, not authorization — dismissing a site-wide
791 + // notice deletes an option, so require a capability as well.
792 + if (!current_user_can('edit_posts')) {
793 + wp_die(-1, 403);
794 + }
795 +
717 796 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
718 797
719 798 if ($notice_type === 'welcome') {
720 799 delete_option('thinkrank_show_welcome');
@@ -742,8 +821,9 @@
742 821 *
743 822 * @return string Menu icon
744 823 */
745 824 private function get_menu_icon(): string {
825 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- a data: URI for the menu icon must be base64.
746 826 return 'data:image/svg+xml;base64,' . base64_encode(
747 827 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
748 828 <g clipPath="url(#thinkrank-clip)">
749 829 <g filter="url(#thinkrank-shadow)">
@@ -791,9 +871,9 @@
791 871 */
792 872 private function get_admin_settings(): array {
793 873 return [
794 874
795 - 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
875 + 'ai_provider' => $this->settings->get('ai_provider', Settings::AI_PROVIDER_NONE),
796 876 'cache_duration' => $this->settings->get('cache_duration', 3600),
797 877 ];
798 878 }
799 879
@@ -925,10 +1005,11 @@
925 1005 'thinkrank_page_thinkrank-ai-tools',
926 1006 'thinkrank_page_thinkrank-settings',
927 1007 'thinkrank_page_thinkrank-usages',
928 1008 'thinkrank_page_thinkrank-license',
1009 + 'thinkrank_page_thinkrank-import-export',
929 1010 'thinkrank_page_thinkrank-migration'
930 - ] ) ) {
1011 + ] , true) ) {
931 1012
932 1013 remove_all_actions( 'user_admin_notices' );
933 1014 remove_all_actions( 'admin_notices' );
934 1015 remove_all_actions( 'all_admin_notices' );