PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.27.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.27.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
thinkrank / includes / admin / class-manager.php

class-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.27.0, at includes/admin/class-manager.php

944 lines 33.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Admin Manager Class
5 *
6 * Handles WordPress admin interface integration
7 *
8 * @package ThinkRank\Admin
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\Admin;
15
16 use ThinkRank\Core\Settings;
17 use ThinkRank\Core\Database;
18 use ThinkRank\Core\Plan_Config;
19 use ThinkRank\Core\Capability_Manager;
20 use ThinkRank\Admin\Metabox_Manager;
21 use ThinkRank\Admin\Elementor_Metabox;
22 use ThinkRank\Admin\Oxygen_Metabox;
23 use ThinkRank\Admin\Divi_Metabox;
24 use ThinkRank\Admin\Bulk_Action_Manager;
25 use ThinkRank\Admin\Post_List_Filters;
26
27 // Prevent direct access
28 if (!defined('ABSPATH')) {
29 exit;
30 }
31
32 /**
33 * Admin Manager Class
34 *
35 * Single Responsibility: Manage WordPress admin interface
36 *
37 * @since 1.0.0
38 */
39 class Manager {
40
41 /**
42 * Settings instance
43 *
44 * @var Settings
45 */
46 private Settings $settings;
47
48 /**
49 * Database instance
50 *
51 * @var Database
52 */
53 private Database $database;
54
55 /**
56 * Metabox manager instance
57 *
58 * @var Metabox_Manager
59 */
60 private Metabox_Manager $metabox_manager;
61
62 /**
63 * Elementor editor metabox integration instance
64 *
65 * @var Elementor_Metabox
66 */
67 private Elementor_Metabox $elementor_metabox;
68
69 /**
70 * Oxygen / Breakdance editor metabox integration instance
71 *
72 * @var Oxygen_Metabox
73 */
74 private Oxygen_Metabox $oxygen_metabox;
75
76 /**
77 * Divi Visual Builder metabox integration instance
78 *
79 * @var Divi_Metabox
80 */
81 private Divi_Metabox $divi_metabox;
82
83 /**
84 * Post list columns instance
85 *
86 * @var Post_List_Columns
87 */
88 private Post_List_Columns $post_list_columns;
89
90 /**
91 * Focus keyword AJAX handler instance
92 *
93 * @var Focus_Keyword_Ajax
94 */
95 private Focus_Keyword_Ajax $focus_keyword_ajax;
96
97 /**
98 * Bulk action manager instance
99 *
100 * @var Bulk_Action_Manager
101 */
102 private Bulk_Action_Manager $bulk_action_manager;
103
104 /**
105 * Post list filters instance
106 *
107 * @var Post_List_Filters
108 */
109 private Post_List_Filters $post_list_filters;
110
111 /**
112 * Admin pages
113 *
114 * @var array
115 */
116 private array $pages = [];
117
118 /**
119 * Constructor
120 *
121 * @param Settings $settings Settings instance
122 * @param Database $database Database instance
123 */
124 public function __construct(?Settings $settings = null, ?Database $database = null) {
125 $this->settings = $settings ?? Settings::instance();
126 $this->database = $database ?? new Database();
127 $this->metabox_manager = new Metabox_Manager($this->settings);
128 $this->elementor_metabox = new Elementor_Metabox($this->metabox_manager);
129 $this->oxygen_metabox = new Oxygen_Metabox($this->metabox_manager);
130 $this->divi_metabox = new Divi_Metabox($this->metabox_manager);
131 $this->post_list_columns = new Post_List_Columns();
132 $this->focus_keyword_ajax = new Focus_Keyword_Ajax();
133 $this->bulk_action_manager = new Bulk_Action_Manager();
134 $this->post_list_filters = new Post_List_Filters();
135 }
136
137 /**
138 * Initialize admin interface
139 *
140 * @return void
141 */
142 public function init(): void {
143 add_action('admin_menu', [$this, 'add_admin_menu']);
144 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
145 add_action('admin_init', [$this, 'handle_admin_init']);
146 add_action('admin_notices', [$this, 'show_admin_notices']);
147 add_action('thinkrank_admin_notices', [$this, 'show_admin_notices']);
148 add_action( 'in_admin_header', [ $this, 'remove_admin_notice' ], 99 );
149
150 // AJAX handlers
151 add_action('wp_ajax_thinkrank_dismiss_notice', [$this, 'dismiss_notice']);
152
153 // Initialize metabox manager
154 $this->metabox_manager->init();
155
156 // Initialize Elementor editor integration (hooks no-op without Elementor)
157 $this->elementor_metabox->init();
158
159 // Initialize Oxygen / Breakdance editor integration (hooks gate on the
160 // builder request, so they no-op without Oxygen)
161 $this->oxygen_metabox->init();
162
163 // Initialize Divi Visual Builder integration (hooks gate on the VB
164 // request, so they no-op without Divi)
165 $this->divi_metabox->init();
166
167 // Initialize post list columns
168 $this->post_list_columns->init();
169
170 // Initialize focus keyword AJAX handler
171 $this->focus_keyword_ajax->init();
172
173 // Initialize Bulk Action Manager
174 $this->bulk_action_manager->init();
175
176 // Initialize Post List Filters
177 $this->post_list_filters->init();
178
179 // Initialize Setup Wizard (onboarding) controller
180 (new Setup_Wizard())->init();
181
182 // Initialize the wp-admin Dashboard widget (ThinkRank Website Insights)
183 (new Dashboard_Widget())->init();
184 }
185
186 /**
187 * Add admin menu pages
188 *
189 * @return void
190 */
191 public function add_admin_menu(): void {
192 // Main menu page
193 $this->pages['dashboard'] = add_menu_page(
194 __('ThinkRank', 'thinkrank'),
195 __('ThinkRank', 'thinkrank'),
196 Capability_Manager::ACCESS,
197 'thinkrank',
198 [$this, 'render_dashboard_page'],
199 $this->get_menu_icon(),
200 30
201 );
202
203 // Dashboard submenu (same as main)
204 $this->pages['dashboard_sub'] = add_submenu_page(
205 'thinkrank',
206 __('Dashboard', 'thinkrank'),
207 __('Dashboard', 'thinkrank'),
208 Capability_Manager::ACCESS,
209 'thinkrank',
210 [$this, 'render_dashboard_page']
211 );
212
213 // Essential SEO page (React tabbed interface)
214 $this->pages['essential_seo'] = add_submenu_page(
215 'thinkrank',
216 __('Essential SEO', 'thinkrank'),
217 __('Essential SEO', 'thinkrank'),
218 Capability_Manager::ACCESS,
219 'thinkrank-essential-seo',
220 [$this, 'render_essential_seo_page']
221 );
222
223 // AI Tools page — gated by the AI Tools section capability.
224 $this->pages['ai_tools'] = add_submenu_page(
225 'thinkrank',
226 __('AI Tools', 'thinkrank'),
227 __('AI Tools', 'thinkrank'),
228 'thinkrank_content_tools',
229 'thinkrank-ai-tools',
230 [$this, 'render_ai_tools_page']
231 );
232
233 // Usages page — gated by the Analytics section capability.
234 $this->pages['analytics'] = add_submenu_page(
235 'thinkrank',
236 __('Usages', 'thinkrank'),
237 __('Usages', 'thinkrank'),
238 'thinkrank_analytics',
239 'thinkrank-usages',
240 [$this, 'render_analytics_page']
241 );
242
243 // Settings page — gated by the Settings & API Keys section capability.
244 $this->pages['settings'] = add_submenu_page(
245 'thinkrank',
246 __('Settings', 'thinkrank'),
247 __('Settings', 'thinkrank'),
248 'thinkrank_settings',
249 'thinkrank-settings',
250 [$this, 'render_settings_page']
251 );
252
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)) {
258 $this->pages['migration'] = add_submenu_page(
259 'thinkrank',
260 __('Migration', 'thinkrank'),
261 __('Migration', 'thinkrank'),
262 'manage_options',
263 'thinkrank-migration',
264 [$this, 'render_migration_page']
265 );
266 }
267
268 // Hook for page-specific initialization
269 foreach ($this->pages as $page_hook) {
270 add_action("load-{$page_hook}", [$this, 'load_admin_page']);
271 }
272 }
273
274 /**
275 * Enqueue admin scripts and styles
276 *
277 * APPROACH: Manual enqueuing with disabled webpack code splitting
278 * - All dependencies bundled into main admin.js (677KB)
279 * - Chart.js separated into charts.js (138KB) for performance
280 * - No dynamic chunks - predictable loading order
281 *
282 * @param string $hook_suffix Current admin page hook
283 * @return void
284 */
285 public function enqueue_admin_scripts(string $hook_suffix): void {
286 // Only load on our admin pages
287 if (!in_array($hook_suffix, $this->pages, true)) {
288 return;
289 }
290
291 // Get asset files for cache busting
292 $admin_asset_file = THINKRANK_PLUGIN_DIR . 'assets/admin.asset.php';
293 $admin_asset_data = file_exists($admin_asset_file) ? include $admin_asset_file : [
294 'dependencies' => [],
295 'version' => THINKRANK_VERSION,
296 ];
297
298 // Enqueue the Chart.js bundle on every ThinkRank page: the admin app
299 // is a SPA, so chart pages (Usages, Essential SEO Performance) are
300 // reachable from any other ThinkRank page without a reload.
301 $charts_asset_file = THINKRANK_PLUGIN_DIR . 'assets/charts.asset.php';
302 $should_enqueue_charts = true;
303
304 if ($should_enqueue_charts && file_exists($charts_asset_file)) {
305 $charts_asset_data = include $charts_asset_file;
306 wp_enqueue_script(
307 'thinkrank-charts',
308 THINKRANK_PLUGIN_URL . 'assets/charts.js',
309 $charts_asset_data['dependencies'],
310 $charts_asset_data['version'],
311 true
312 );
313 // Add charts as dependency for admin script to ensure registration before use
314 $admin_dependencies = array_merge($admin_asset_data['dependencies'], ['thinkrank-charts']);
315 } else {
316 // Do not load charts on pages that don't need it
317 $admin_dependencies = $admin_asset_data['dependencies'];
318 }
319
320 wp_enqueue_script(
321 'thinkrank-admin',
322 THINKRANK_PLUGIN_URL . 'assets/admin.js',
323 $admin_dependencies,
324 $admin_asset_data['version'],
325 true
326 );
327
328 // Add defer attribute for better performance
329 wp_script_add_data('thinkrank-admin', 'defer', true);
330
331 // Enqueue admin styles
332 wp_enqueue_style(
333 'thinkrank-admin',
334 THINKRANK_PLUGIN_URL . 'assets/admin.css',
335 ['wp-components'],
336 $admin_asset_data['version']
337 );
338
339 // Enqueue WordPress media library for MediaPicker component
340 wp_enqueue_media();
341
342 // Preload the REST responses every ThinkRank admin page requests on
343 // mount (Site Kit pattern: rest_preload_api_request piped into an
344 // apiFetch preloading middleware) so first paint needs zero
345 // round-trips for them. Only cheap, local settings endpoints belong
346 // here — never Google-backed report data.
347 $preload_paths = apply_filters('thinkrank_apifetch_preload_paths', [
348 '/thinkrank/v1/site-identity/settings',
349 '/thinkrank/v1/site-identity/title/templates',
350 '/thinkrank/v1/site-identity/breadcrumbs/types',
351 ]);
352 $preload_data = array_reduce($preload_paths, 'rest_preload_api_request', []);
353 wp_add_inline_script(
354 'thinkrank-admin',
355 sprintf('window.thinkrankApiPreload = %s;', wp_json_encode((object) $preload_data)),
356 'before'
357 );
358
359 // Site info saved in ThinkRank Site Identity takes precedence over
360 // the WordPress defaults so previews reflect what the user saved.
361 $site_identity_settings = (new \ThinkRank\SEO\Site_Identity_Manager())->get_settings('site');
362
363 // Localize script with data
364 wp_localize_script('thinkrank-admin', 'thinkrankAdmin', [
365 'apiUrl' => rest_url('thinkrank/v1/'),
366 'restNonce' => wp_create_nonce('wp_rest'),
367 'adminNonce' => wp_create_nonce('thinkrank_admin'),
368 'currentUser' => wp_get_current_user()->ID,
369 'displayName' => wp_get_current_user()->display_name,
370 'capabilities' => $this->get_user_capabilities(),
371 'settings' => $this->get_admin_settings(),
372 'i18n' => $this->get_i18n_strings(),
373 'isAdmin' => current_user_can('manage_options'),
374 // Whether any AI provider API key is configured — used to gate
375 // "Generate with AI" buttons in the UI
376 'aiConfigured' => $this->is_ai_configured(),
377 // Plugin version - directly available without API call
378 'version' => THINKRANK_VERSION,
379 // Site information for default values
380 'siteName' => !empty($site_identity_settings['site_name']) ? $site_identity_settings['site_name'] : get_bloginfo('name'),
381 'siteDescription' => !empty($site_identity_settings['site_description']) ? $site_identity_settings['site_description'] : get_bloginfo('description'),
382 'siteUrl' => home_url(),
383 'faviconUrl' => get_site_icon_url(64) ?: '',
384 'adminEmail' => get_option('admin_email'),
385 // Post types for Global SEO navigation
386 'postTypes' => $this->get_public_post_types(),
387 // Role Manager: capabilities the current user holds + the
388 // section → capability map, so the SPA can hide sections a role
389 // cannot access. Administrators receive every capability.
390 'caps' => Capability_Manager::user_capabilities(),
391 'sectionCaps' => Capability_Manager::section_map(),
392 'canManageRoles' => Capability_Manager::current_user_can(Capability_Manager::MANAGE_ROLES),
393 // Pro detection flag
394 '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(),
400 // MCP (Model Context Protocol) connection details for the MCP page.
401 'mcp' => $this->get_mcp_globals(),
402 // Google OAuth: JS only ever gets a nonce-signed admin-post URL.
403 // The consent URL, client ID, and scopes are assembled by the proxy,
404 // so no Google app credentials reach the browser or the bundle.
405 'googleOAuth' => [
406 'connectUrl' => \ThinkRank\Integrations\Google_OAuth_Proxy::get_connect_url(),
407 // '' when fine, otherwise why re-authorization is needed:
408 // 'contract' (upgraded off the old token flow) or
409 // 'credentials' (stored tokens no longer decryptable).
410 'reconnectReason' => (string) get_option('thinkrank_google_reconnect_required', ''),
411 ],
412 ]);
413
414 }
415
416 /**
417 * Handle admin initialization
418 *
419 * @return void
420 */
421 public function handle_admin_init(): void {
422 // Show welcome screen for new installations (only if no API key configured)
423 if (get_option('thinkrank_show_welcome') && !$this->has_api_key_configured()) {
424 add_action('admin_notices', [$this, 'show_welcome_notice']);
425 }
426
427 // Check for plugin updates
428 $this->check_plugin_updates();
429 }
430
431 /**
432 * Load admin page
433 *
434 * @return void
435 */
436 public function load_admin_page(): void {
437 // Add screen options
438 $this->add_screen_options();
439 }
440
441 /**
442 * Render dashboard page
443 *
444 * @return void
445 */
446 public function render_dashboard_page(): void {
447 $this->render_admin_page('dashboard', [
448 'title' => __('ThinkRank Dashboard', 'thinkrank'),
449 'description' => __('AI-powered SEO optimization for WordPress', 'thinkrank'),
450 ]);
451 }
452
453 /**
454 * Render Essential SEO page
455 *
456 * @return void
457 */
458 public function render_essential_seo_page(): void {
459 $this->render_admin_page('essential-seo', [
460 'title' => __('Essential SEO', 'thinkrank'),
461 'description' => __('Configure your site-wide SEO settings with AI-powered optimization', 'thinkrank'),
462 ]);
463 }
464
465 /**
466 * Render AI Tools page
467 *
468 * @return void
469 */
470 public function render_ai_tools_page(): void {
471 $this->render_admin_page('ai-tools', [
472 'title' => __('AI Tools', 'thinkrank'),
473 'description' => __('AI-powered content tools including Content Planner and Metadata Generator', 'thinkrank'),
474 ]);
475 }
476
477 /**
478 * Render settings page
479 *
480 * @return void
481 */
482 public function render_settings_page(): void {
483 $this->render_admin_page('settings', [
484 'title' => __('ThinkRank Settings', 'thinkrank'),
485 'description' => __('Configure your AI SEO settings', 'thinkrank'),
486 ]);
487 }
488
489 /**
490 * Build the MCP connection globals passed to the admin app.
491 *
492 * The MCP page fetches live connection state from the
493 * /thinkrank/v1/mcp/connection route; these globals only carry what the
494 * page needs before that request resolves (endpoint URLs and whether the
495 * bundled Abilities API — the tool catalog — is available).
496 *
497 * @return array<string, mixed>
498 */
499 private function get_mcp_globals(): array {
500 // The tool catalog is the abilities registry; wp_register_ability
501 // comes from the bundled Abilities API under dependencies/. When it's
502 // missing (bundle not built), the MCP server has no tools to serve.
503 $abilities_api_available = function_exists('wp_register_ability');
504
505 return [
506 'abilities_api_available' => $abilities_api_available,
507 'mcp_endpoint' => \ThinkRank\Mcp\Mcp_Pairing::site_endpoint(),
508 'mcp_endpoint_rest' => \ThinkRank\Mcp\Mcp_Pairing::site_endpoint_fallback(),
509 ];
510 }
511
512
513
514 /**
515 * Render usage analytics page
516 *
517 * @return void
518 */
519 public function render_analytics_page(): void {
520 $this->render_admin_page('analytics', [
521 'title' => __('Usage Analytics', 'thinkrank'),
522 'description' => __('Track your AI usage, costs, and plugin performance analytics', 'thinkrank'),
523 ]);
524 }
525
526 /**
527 * Render import/export page
528 *
529 * @return void
530 */
531 public function render_import_export_page(): void {
532 $this->render_admin_page('import-export', [
533 'page_title' => __('Import / Export', 'thinkrank'),
534 ]);
535 }
536
537 /**
538 * Render the Migration page (re-run SEO data imports).
539 *
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.
542 *
543 * @return void
544 */
545 public function render_migration_page(): void {
546 if (!Settings::instance()->get('enable_migration_tools', false)) {
547 wp_die(esc_html__('The Migration tools are not enabled.', 'thinkrank'));
548 }
549 $this->render_admin_page('migration', [
550 'page_title' => __('Migration', 'thinkrank'),
551 ]);
552 }
553
554 /**
555 * Render admin page template
556 *
557 * @param string $page Page identifier
558 * @param array $data Page data
559 * @return void
560 */
561 private function render_admin_page(string $page, array $data): void {
562 ?>
563 <div class="wrap">
564 <div id="thinkrank-<?php echo esc_attr($page); ?>" class="thinkrank-admin-page"></div>
565 </div>
566 <?php
567 }
568
569 /**
570 * Add meta boxes to post edit screens
571 *
572 * @return void
573 */
574 public function add_meta_boxes(): void {
575 $post_types = get_post_types(['public' => true]);
576
577 foreach ($post_types as $post_type) {
578 add_meta_box(
579 'thinkrank-seo',
580 __('ThinkRank SEO', 'thinkrank'),
581 [$this, 'render_seo_meta_box'],
582 $post_type,
583 'normal',
584 'high'
585 );
586 }
587 }
588
589 /**
590 * Render SEO meta box
591 *
592 * @param \WP_Post $post Current post object
593 * @return void
594 */
595 public function render_seo_meta_box(\WP_Post $post): void {
596 wp_nonce_field('thinkrank_meta_box', 'thinkrank_meta_box_nonce');
597
598 echo '<div id="thinkrank-meta-box" data-post-id="' . esc_attr($post->ID) . '">';
599 echo '</div>';
600 }
601
602 /**
603 * Save meta box data
604 *
605 * @param int $post_id Post ID
606 * @return void
607 */
608 public function save_meta_boxes(int $post_id): void {
609 // Verify nonce
610 if (!isset($_POST['thinkrank_meta_box_nonce'])) {
611 return;
612 }
613
614 $nonce = sanitize_text_field(wp_unslash($_POST['thinkrank_meta_box_nonce']));
615 if (!wp_verify_nonce($nonce, 'thinkrank_meta_box')) {
616 return;
617 }
618
619 // Check permissions
620 if (!current_user_can('edit_post', $post_id)) {
621 return;
622 }
623
624 // Save meta data (handled by AJAX in React components)
625 // Pass only sanitized ThinkRank-related fields to action hook
626 $sanitized_data = [];
627 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
628 foreach ($_POST as $key => $value) {
629 if (strpos($key, 'thinkrank_') === 0 || strpos($key, '_thinkrank_') === 0) {
630 $sanitized_data[$key] = is_array($value)
631 ? array_map('sanitize_text_field', wp_unslash($value))
632 : sanitize_text_field(wp_unslash($value));
633 }
634 }
635 do_action('thinkrank_save_post_meta', $post_id, $sanitized_data);
636 }
637
638 /**
639 * Show admin notices
640 *
641 * @return void
642 */
643 public function show_admin_notices(): void {
644 // Implementation will be added in next iteration
645 }
646
647 /**
648 * Show welcome notice
649 *
650 * @return void
651 */
652 public function show_welcome_notice(): void {
653 wp_enqueue_style(
654 'thinkrank-admin-notices',
655 THINKRANK_PLUGIN_URL . 'static/css/admin-notices.css',
656 [],
657 THINKRANK_VERSION
658 );
659 ?>
660 <div class="notice notice-success is-dismissible thinkrank-notice thinkrank-welcome-notice">
661 <div class="thinkrank-notice__inner">
662 <div class="thinkrank-notice__body">
663 <p class="thinkrank-notice__title"><?php esc_html_e('Welcome to ThinkRank!', 'thinkrank'); ?></p>
664 <p class="thinkrank-notice__text"><?php esc_html_e('Thanks for installing ThinkRank. Add an AI provider key to unlock automatic titles, descriptions, and SEO scoring.', 'thinkrank'); ?></p>
665 <p class="thinkrank-notice__actions">
666 <a href="<?php echo esc_url(admin_url('admin.php?page=thinkrank-settings')); ?>" class="button button-primary">
667 <?php esc_html_e('Configure Settings', 'thinkrank'); ?>
668 </a>
669 <a href="#" class="thinkrank-notice__dismiss thinkrank-dismiss-welcome" data-nonce="<?php echo esc_attr(wp_create_nonce('thinkrank_admin')); ?>">
670 <?php esc_html_e('Dismiss', 'thinkrank'); ?>
671 </a>
672 </p>
673 </div>
674 </div>
675 </div>
676 <?php
677 // The notice renders on every admin screen, so the dismiss handler must
678 // ship with it — the thinkrank-admin bundle only loads on ThinkRank pages.
679 // Persist the dismissal for both our "Dismiss" link and core's × button.
680 wp_print_inline_script_tag(
681 '( function () {
682 document.addEventListener( "click", function ( event ) {
683 var notice = event.target.closest( ".thinkrank-welcome-notice" );
684 if ( ! notice ) {
685 return;
686 }
687 var link = event.target.closest( ".thinkrank-dismiss-welcome" );
688 if ( ! link && ! event.target.closest( ".notice-dismiss" ) ) {
689 return;
690 }
691 if ( link ) {
692 event.preventDefault();
693 notice.style.display = "none";
694 }
695 window.fetch( window.ajaxurl, {
696 method: "POST",
697 credentials: "same-origin",
698 body: new URLSearchParams( {
699 action: "thinkrank_dismiss_notice",
700 notice_type: "welcome",
701 nonce: notice.querySelector( ".thinkrank-dismiss-welcome" ).dataset.nonce,
702 } ),
703 } );
704 } );
705 } )();'
706 );
707 }
708
709 /**
710 * Dismiss notice via AJAX
711 *
712 * @return void
713 */
714 public function dismiss_notice(): void {
715 check_ajax_referer('thinkrank_admin', 'nonce');
716
717 $notice_type = sanitize_key($_POST['notice_type'] ?? '');
718
719 if ($notice_type === 'welcome') {
720 delete_option('thinkrank_show_welcome');
721 }
722
723 wp_die();
724 }
725
726 /**
727 * Check if API key is configured
728 *
729 * @return bool True if at least one API key is configured
730 */
731 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'));
738 }
739
740 /**
741 * Get menu icon
742 *
743 * @return string Menu icon
744 */
745 private function get_menu_icon(): string {
746 return 'data:image/svg+xml;base64,' . base64_encode(
747 '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
748 <g clipPath="url(#thinkrank-clip)">
749 <g filter="url(#thinkrank-shadow)">
750 <circle cx="14" cy="9.2" r="1.3" fill="#a7aaad"/>
751 </g>
752 <path d="M19.5 7v5.3c0 2.4 0 3.6-.5 4.5-.4.8-1 1.4-1.8 1.8-.9.5-2.1.5-4.5.5H7.3c-2.4 0-3.6 0-4.5-.5-.8-.4-1.4-1-1.8-1.8-.2-.3-.3-.7-.4-1.1.5-.4.9-.6 1.3-.7.5-.2.9-.2 1.4-.2.7.1 1.3.2 2 .3.7.1 1.4.2 2.1.1 1.5-.3 2.5-1 3.4-2 .5-.5.9-1 1.4-1.5.3-.3.6-.7.9-1 .3.1.6.2.9.2.9 0 1.7-.8 1.7-1.7 0-.2 0-.4-.1-.6.7-.4 1.4-.8 2.1-1.1.6-.3 1.2-.6 1.6-.8v.4zM12.5 0c2.4 0 3.6 0 4.5.5.8.4 1.4 1 1.8 1.8.4.7.5 1.6.5 3.1-.1 0-.2.1-.3.1-.5.2-1.1.5-1.8.8-.7.3-1.5.7-2.2 1.1-.3-.3-.7-.4-1.1-.4-.9 0-1.7.8-1.7 1.7 0 .3.1.6.3.9-.3.4-.6.7-.9 1-.5.6-.9 1.1-1.3 1.5-.9.9-1.8 1.5-3 1.7-.6.1-1.2.1-1.8 0-.6-.1-1.3-.3-2-.4-.6-.1-1.2-.1-1.8.1-.3.1-.7.3-1 .5 0-.6 0-1.4 0-2.3V7c0-2.4 0-3.6.5-4.5.4-.8 1-1.4 1.8-1.8C3.9 0 5.1 0 7.5 0h5zm-5.8 8.2c0-.1-.1-.1-.2 0l-.2.9c0 0 0 .1-.1.1l-.9.2c-.1 0-.1.1 0 .1l.9.2c0 0 .1 0 .1.1l.2.9c0 .1.1.1.2 0l.2-.9c0 0 0-.1.1-.1l.9-.2c.1 0 .1-.1 0-.1l-.9-.2c0 0-.1 0-.1-.1l-.2-.9zm8.8-.4c0 .1 0 .2 0 .3 0 .7-.6 1.3-1.3 1.3-.2 0-.4 0-.5-.1.1-.1.2-.2.3-.3.4-.4.9-.8 1.5-1.2zm-1.3-1c.3 0 .5.1.7.2-.6.4-1.1.8-1.5 1.2l-.1.1c-.1.1-.2.2-.3.3-.1-.2-.1-.4-.1-.6 0-.7.6-1.2 1.3-1.2zM8.6 2.5c-.1-.2-.4-.2-.4 0l-.4 1.4c0 .1-.1.1-.1.1L6.2 4.4c-.2.1-.2.4 0 .4l1.4.4c.1 0 .1.1.1.1l.4 1.4c.1.2.4.2.4 0l.4-1.4c0-.1.1-.1.1-.1l1.4-.4c.2-.1.2-.4 0-.4L8.9 4c-.1 0-.1-.1-.1-.1L8.6 2.5z" fill="#a7aaad"/>
753 </g>
754 <defs>
755 <filter id="thinkrank-shadow" x="11.2" y="7.2" width="5.6" height="5.6" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
756 <feFlood floodOpacity="0" result="BackgroundImageFix"/>
757 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
758 <feOffset dy="0.7"/>
759 <feGaussianBlur stdDeviation="0.7"/>
760 <feComposite in2="hardAlpha" operator="out"/>
761 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
762 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
763 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
764 </filter>
765 <clipPath id="thinkrank-clip">
766 <rect width="20" height="20" rx="5.5" fill="white"/>
767 </clipPath>
768 </defs>
769 </svg>'
770 );
771 }
772
773 /**
774 * Get user capabilities for current user
775 *
776 * @return array User capabilities
777 */
778 private function get_user_capabilities(): array {
779 return [
780 'manage_settings' => current_user_can('manage_options'),
781 'view_analytics' => current_user_can('edit_posts'),
782
783 'use_ai_features' => current_user_can('edit_posts'),
784 ];
785 }
786
787 /**
788 * Get admin settings for JavaScript
789 *
790 * @return array Admin settings
791 */
792 private function get_admin_settings(): array {
793 return [
794
795 'ai_provider' => $this->settings->get('ai_provider', 'openai'),
796 'cache_duration' => $this->settings->get('cache_duration', 3600),
797 ];
798 }
799
800 /**
801 * Whether any AI provider API key is configured
802 *
803 * Mirrors the check used by ThinkRank\AI\Manager.
804 *
805 * @return bool
806 */
807 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;
815 }
816
817 /**
818 * Get internationalization strings
819 *
820 * @return array I18n strings
821 */
822 private function get_i18n_strings(): array {
823 return [
824 'loading' => __('Loading...', 'thinkrank'),
825 'error' => __('An error occurred', 'thinkrank'),
826 'success' => __('Success!', 'thinkrank'),
827 'confirm' => __('Are you sure?', 'thinkrank'),
828 'cancel' => __('Cancel', 'thinkrank'),
829 'save' => __('Save', 'thinkrank'),
830 ];
831 }
832
833 /**
834 * Get all public post types for SEO configuration
835 *
836 * @return array Post types data
837 */
838 private function get_public_post_types(): array {
839 // Get all public post types (both built-in and custom)
840 $post_types = get_post_types([
841 'public' => true
842 ], 'objects');
843
844 $post_types_data = [];
845
846 foreach ($post_types as $post_type) {
847 // Shared eligibility policy (viewable + deny list) so the UI list and
848 // the REST/ability write paths agree on which types are SEO targets.
849 if (!\ThinkRank\SEO\Global_SEO_Post_Types::is_allowed($post_type)) {
850 continue;
851 }
852
853 $post_types_data[] = [
854 'name' => $post_type->name,
855 'slug' => $post_type->name,
856 'label' => $post_type->label,
857 'singular_name' => $post_type->labels->singular_name ?? $post_type->label,
858 'plural_name' => $post_type->label,
859 'public' => $post_type->public,
860 'has_archive' => $post_type->has_archive,
861 'hierarchical' => $post_type->hierarchical,
862 ];
863 }
864
865 return $post_types_data;
866 }
867
868 /**
869 * Add help tabs
870 *
871 * @return void
872 */
873 private function add_help_tabs(): void {
874 $screen = get_current_screen();
875
876 $screen->add_help_tab([
877 'id' => 'thinkrank-overview',
878 'title' => __('Overview', 'thinkrank'),
879 'content' => '<p>' . __('ThinkRank.ai helps you optimize your content with AI-powered SEO suggestions.', 'thinkrank') . '</p>',
880 ]);
881
882 $screen->set_help_sidebar(
883 '<p><strong>' . __('For more information:', 'thinkrank') . '</strong></p>' .
884 '<p><a href="https://thinkrank.ai/docs" target="_blank">' . __('Documentation', 'thinkrank') . '</a></p>' .
885 '<p><a href="https://wpdeveloper.com/support/new-ticket/" target="_blank">' . __('Support', 'thinkrank') . '</a></p>'
886 );
887 }
888
889 /**
890 * Add screen options
891 *
892 * @return void
893 */
894 private function add_screen_options(): void {
895 // Screen options will be added as needed
896 }
897
898 /**
899 * Check for plugin updates
900 *
901 * @return void
902 */
903 private function check_plugin_updates(): void {
904 $current_version = get_option('thinkrank_version');
905
906 if (false === $current_version) {
907 // Fresh install or missing option — record version without firing the update hook.
908 update_option('thinkrank_version', THINKRANK_VERSION);
909 return;
910 }
911
912 if (version_compare($current_version, THINKRANK_VERSION, '<')) {
913 // Handle plugin update
914 do_action('thinkrank_plugin_updated', $current_version, THINKRANK_VERSION);
915 update_option('thinkrank_version', THINKRANK_VERSION);
916 }
917 }
918
919
920 public function remove_admin_notice() {
921 $current_screen = get_current_screen();
922 if ( in_array( $current_screen->id, [
923 'toplevel_page_thinkrank',
924 'thinkrank_page_thinkrank-essential-seo',
925 'thinkrank_page_thinkrank-ai-tools',
926 'thinkrank_page_thinkrank-settings',
927 'thinkrank_page_thinkrank-usages',
928 'thinkrank_page_thinkrank-license',
929 'thinkrank_page_thinkrank-migration'
930 ] ) ) {
931
932 remove_all_actions( 'user_admin_notices' );
933 remove_all_actions( 'admin_notices' );
934 remove_all_actions( 'all_admin_notices' );
935 remove_all_actions( 'network_admin_notices' );
936
937 // To showing notice in EA settings page we have to use 'eael_admin_notices' action hook
938 add_action( 'admin_notices', function () {
939 do_action( 'thinkrank_admin_notices' );
940 } );
941 }
942 }
943 }
944