PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Core / Admin.php

Admin.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.0, at includes/Core/Admin.php

1,433 lines 48.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Core;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8
9 use Exception;
10 use PriyoMukul\WPNotice\Notices;
11 use WPDeveloper\BetterDocs\Admin\NoticePointers;
12 use WPDeveloper\BetterDocs\Utils\Base;
13 use PriyoMukul\WPNotice\Utils\CacheBank;
14 use WPDeveloper\BetterDocs\Utils\Helper;
15 use WPDeveloper\BetterDocs\Utils\Enqueue;
16 use WPDeveloper\BetterDocs\Insights\Insights;
17 use PriyoMukul\WPNotice\Utils\NoticeRemover;
18 use WPDeveloper\BetterDocs\Core\PluginInstaller;
19 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
20
21 class Admin extends Base {
22 /**
23 * @var CacheBank
24 */
25 private static $cache_bank;
26 /**
27 * Admin Root Menu Slug
28 *
29 * @var string
30 */
31 private $slug = 'betterdocs-dashboard';
32 /**
33 * Insights
34 *
35 * @var Insights
36 */
37 private $insights = null;
38
39 /**
40 * DI\Container
41 *
42 * @var Container
43 */
44 private $container;
45
46 /**
47 * Database Wrapper
48 *
49 * @var Settings
50 */
51 private $settings;
52
53 /**
54 * KBMigration
55 *
56 * @var KBMigration
57 */
58 private $kbmigration;
59
60 /**
61 * Enqueue
62 *
63 * @var Enqueue
64 */
65 private $assets;
66
67 // modules
68 protected $installer;
69
70 /**
71 * FAQBuilder
72 *
73 * @var FAQBuilder
74 */
75 private $faq_builder;
76 private $glossaries;
77
78 public function __construct( Container $container, PostType $type, Enqueue $assets, Settings $settings, KBMigration $kbmigration ) {
79 $this->container = $container;
80 $this->assets = $assets;
81 $this->settings = $settings;
82 $this->kbmigration = $kbmigration;
83 $this->slug = 'betterdocs-dashboard';
84
85 add_action( 'init', array( $type, 'register' ), 9 );
86 add_action( 'rest_api_init', array( $this, 'order_terms_in_wp_terms_admin_table' ) );
87
88 $type->init();
89 $type->admin_init();
90
91 $this->faq_builder = $this->container->get( FAQBuilder::class );
92 $this->glossaries = $this->container->get( Glossaries::class );
93
94 /**
95 * Register usage tracking (including the daily `put_do_weekly_action` cron
96 * handler) on every request — WP-Cron runs with is_admin() === false, so
97 * this MUST sit above the admin guard or the cron send never fires. The
98 * admin-only UI hooks inside Insights::init() (deactivation form, footer
99 * scripts, plugin_action_links) are context-specific and simply never run
100 * outside wp-admin.
101 */
102 $this->plugin_insights();
103
104 if ( ! is_admin() ) {
105 return;
106 }
107
108 $this->installer = new PluginInstaller();
109
110 add_action( 'admin_notices', array( $this, 'compatibility_notices' ) );
111 // The WPNotice CacheBank wipes all admin_notices at priority 10 on BetterDocs
112 // screens, so the hook above never renders inside the BetterDocs panels.
113 // Re-add the compatibility notice after that wipe (in_admin_header, priority
114 // 999) so it shows on the panels like the review / license notices.
115 add_action( 'in_admin_header', function () {
116 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
117 if ( $screen && betterdocs()->is_betterdocs_screen( $screen->id ) ) {
118 add_action( 'admin_notices', array( $this, 'compatibility_notices' ) );
119 }
120 }, 999 );
121 // add_action( 'admin_init', [$this, 'notices'], 9 );
122 add_filter( 'admin_init', array( $this, 'save_admin_page' ), 99 );
123
124 add_action( 'admin_menu', array( $this, 'menus' ) );
125 add_action( 'admin_menu', array( $this, 'reset_submenu' ) );
126 add_action( 'admin_head', array( $this, 'add_custom_classes_to_menu_items' ) );
127 add_filter( 'plugin_action_links_' . BETTERDOCS_PLUGIN_BASENAME, array( $this, 'insert_plugin_links' ) );
128
129 // $this->container->get( SetupWizard::class )->init();
130
131 add_action( 'admin_enqueue_scripts', array( $this, 'styles' ) );
132 add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
133 // add_action( 'betterdocs_listing_header', [ $this, 'header' ], 10, 1 );
134 add_action( 'admin_bar_menu', array( $this, 'toolbar_menu' ), 32 );
135
136 add_filter( 'admin_body_class', array( $this, 'body_classes' ) );
137 add_filter( 'parent_file', array( $type, 'highlight_admin_menu' ) );
138 add_filter( 'submenu_file', array( $type, 'highlight_admin_submenu' ), 10, 2 );
139 add_filter( 'betterdocs_admin_menu', array( $this, 'quick_setup_menu' ), 10, 1 );
140
141 /**
142 * Remove Comments Column from List Table.
143 */
144 add_filter( 'manage_docs_posts_columns', array( $this, 'set_custom_edit_action_columns' ) );
145 add_filter( 'manage_docs_posts_custom_column', array( $this, 'manage_custom_columns' ), 10, 2 );
146
147 /**
148 * Add New Column
149 */
150 add_filter( 'manage_users_columns', array( $this, 'add_users_total_docs_column' ), 10, 1 );
151 add_filter( 'manage_users_custom_column', array( $this, 'popular_users_docs_data' ), 10, 3 );
152 if ( is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) {
153 add_action( 'admin_footer-plugins.php', array( $this, 'disable_deactivation' ) );
154 }
155
156 if ( $this->settings->get( 'enable_estimated_reading_time' ) ) {
157 // Hook into unified metabox instead of creating separate metabox
158 add_action( 'betterdocs_reading_time_tab_content', array( $this, 'render_estimated_time_markup' ) );
159 }
160
161 self::$cache_bank = CacheBank::get_instance();
162
163 // Remove OLD notice from 1.0.0 (if other WPDeveloper plugin has notice)
164 NoticeRemover::get_instance( '1.0.0' );
165
166 try {
167 $this->notices();
168 } catch ( Exception $e ) {
169 unset( $e );
170 }
171
172 // Initialize Black Friday Pointer
173 $this->init_black_friday_pointer();
174
175 // Register AJAX handler for pointer dismissal
176 add_action( 'wp_ajax_betterdocs_dismiss_black_friday_pointer', array( $this, 'ajax_dismiss_black_friday_pointer' ) );
177 }
178
179 public function order_terms_in_wp_terms_admin_table() {
180 // order the terms correctly to be shown on the admin panel categories menu with betterdocs order
181 add_action(
182 'rest_insert_doc_category',
183 function ( $term, $request, $bool ) {
184 $max_order = Helper::get_max_doc_category_order_from_term_meta() ?? 0;
185 $next_order = $max_order + 1;
186 update_term_meta( $term->term_id, 'doc_category_order', $next_order );
187 },
188 10,
189 3
190 );
191 }
192
193 public function disable_deactivation() {
194 $tooltip_text = esc_html__( 'Deactivate BetterDocs Pro First', 'betterdocs' );
195 ?>
196 <style type="text/css">
197 #deactivate-betterdocs {
198 color: #cccccc;
199 position: relative;
200 }
201
202 /* Tooltip styling */
203 #deactivate-betterdocs[title]:hover::after {
204 content: attr(title);
205 position: absolute;
206 bottom: 100%;
207 left: 50%;
208 transform: translateX(-50%);
209 background-color: #333;
210 color: #fff;
211 padding: 5px 10px;
212 border-radius: 4px;
213 font-size: 12px;
214 white-space: nowrap;
215 box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
216 z-index: 10;
217 }
218 #deactivate-betterdocs:focus {
219 box-shadow: none;
220 outline: none;
221 }
222 </style>
223 <script type="text/javascript">
224 jQuery(document).ready(function($) {
225 // Disable the default action and add class with tooltip by default
226 const tooltipText = "<?php echo esc_attr( $tooltip_text ); ?>";
227 $("#deactivate-betterdocs")
228 .addClass("disabled-tooltip")
229 .attr("title", tooltipText)
230 .on("click", function(e) {
231 e.preventDefault(); // Prevent any action on click
232 });
233 });
234 </script>
235 <?php
236 }
237
238 public function add_users_total_docs_column( $columns ) {
239 $new_column = array(
240 'docs' => __( 'Docs', 'betterdocs' ),
241 );
242 $columns = array_merge( $columns, $new_column );
243 return $columns;
244 }
245
246 public function popular_users_docs_data( $output, $column_name, $user_id ) {
247 if ( 'docs' == $column_name ) {
248 $total_count = count_user_posts( $user_id, 'docs', true );
249 return '<a href="edit.php?post_type=docs&author=' . $user_id . '" class="edit"><span aria-hidden="true">' . $total_count . '</span></a>';
250 }
251 return $output;
252 }
253
254 public function render_estimated_time_markup() {
255 betterdocs()->views->get( 'admin/metabox/estimated-reading-box' );
256 }
257
258 public function compatibility_notices() {
259 if ( betterdocs()->is_pro_active() ) {
260 $plugins = Helper::get_plugins();
261 $plugin_data = $plugins['betterdocs-pro/betterdocs-pro.php'];
262
263 // Require the paired Pro release: the Analytics UI is version-coupled to
264 // Pro's advanced modules, so an older Pro renders a broken/partial panel.
265 if ( isset( $plugin_data['Version'] ) && version_compare( $plugin_data['Version'], '4.0.0', '>=' ) ) {
266 return;
267 }
268
269 betterdocs()->views->get( 'admin/notices/compatibility', array( 'version' => $plugin_data['Version'] ) );
270 }
271 }
272
273 public function plugin_insights( $prevent_init = false ) {
274 $this->insights = Insights::get_instance(
275 BETTERDOCS_PLUGIN_FILE,
276 array(
277 'opt_in' => true,
278 'goodbye_form' => true,
279 'item_id' => 'c7b16777b4f1b83f6083',
280 )
281 );
282
283 $this->insights->set_notice_options(
284 array(
285 'notice' => __( 'Want to help make <strong>BetterDocs</strong> even more awesome? You can get a <strong>10% discount coupon</strong> for Premium extensions if you allow us to track the usage.', 'betterdocs' ),
286 'extra_notice' => __( 'We collect non-sensitive diagnostic data and plugin usage information. Your site URL, WordPress & PHP version, plugins & themes and email address to send you the discount coupon. This data lets us make sure this plugin always stays compatible with the most popular plugins and themes. No spam, I promise.', 'betterdocs' ),
287 )
288 );
289
290 if ( ! $prevent_init ) {
291 $this->insights->init();
292 }
293
294 return $this->insights;
295 }
296
297 /**
298 * Admin notices for Review and others.
299 *
300 * @return void
301 * @throws Exception
302 */
303 public function notices() {
304 $notices = new Notices(
305 array(
306 'id' => 'betterdocs',
307 'storage_key' => 'notices',
308 'lifetime' => 3,
309 'stylesheet_url' => $this->assets->asset_url( 'admin/css/notices.css' ),
310 'styles' => $this->assets->asset_url( 'admin/css/notices.css' ),
311 'priority' => 4,
312 )
313 );
314
315 /**
316 * Review Notice
317 *
318 * @var mixed $message
319 */
320
321 $message = __( 'We hope you\'re enjoying BetterDocs! Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation?', 'betterdocs' );
322
323 $_review_notice = array(
324 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ),
325 'html' => '<p>' . $message . '</p>',
326 'links' => array(
327 'later' => array(
328 'link' => 'https://wordpress.org/plugins/betterdocs/#reviews',
329 'target' => '_blank',
330 'label' => __( 'Sure, you deserve it!', 'betterdocs' ),
331 'icon_class' => 'dashicons dashicons-external',
332 ),
333 'allready' => array(
334 'label' => __( 'I already did', 'betterdocs' ),
335 'icon_class' => 'dashicons dashicons-smiley',
336 'attributes' => array(
337 'data-dismiss' => true,
338 ),
339 ),
340 'maybe_later' => array(
341 'label' => __( 'Maybe Later', 'betterdocs' ),
342 'icon_class' => 'dashicons dashicons-calendar-alt',
343 'attributes' => array(
344 'data-later' => true,
345 'class' => 'dismiss-btn',
346 ),
347 ),
348 'support' => array(
349 'link' => 'https://wpdeveloper.com/support',
350 'attributes' => array(
351 'target' => '_blank',
352 ),
353 'label' => __( 'I need help', 'betterdocs' ),
354 'icon_class' => 'dashicons dashicons-sos',
355 ),
356 'never_show_again' => array(
357 'label' => __( 'Never show again', 'betterdocs' ),
358 'icon_class' => 'dashicons dashicons-dismiss',
359 'attributes' => array(
360 'data-dismiss' => true,
361 ),
362 ),
363 ),
364 );
365
366 $notices->add(
367 'review',
368 $_review_notice,
369 array(
370 'start' => $notices->strtotime( '+10 days' ),
371 'recurrence' => 30,
372 'dismissible' => true,
373 )
374 );
375
376 if ( $this->kbmigration->existing_plugins && ! in_array( $this->kbmigration->existing_plugins[0][0], $this->kbmigration->migrated_plugins ) ) {
377 $plugin_name = '<strong>' . esc_html( $this->kbmigration->existing_plugins[0][1] ) . '</strong>';
378
379 $message = sprintf(
380 /* translators: %s is the name of the existing knowledge base plugin. */
381 __( 'Already using %s? Power up your Knowledge Base by migrating all your docs and settings to BetterDocs with just 1 click.', 'betterdocs' ),
382 esc_html( $plugin_name )
383 );
384
385 $migration_message = sprintf(
386 '<p class="migration-message">%s</p><a class="button button-primary betterdocs-migration-notice" href="%s">%s</a>',
387 $message,
388 esc_url( admin_url( 'admin.php?page=betterdocs-settings&tab=tab-migration' ) ),
389 esc_html__( 'Start Migration', 'betterdocs' )
390 );
391
392 $_migration_notice = array(
393 'thumbnail' => '',
394 'html' => $migration_message,
395 'links' => array(
396 'maybe_later' => array(
397 'label' => __( 'Maybe Later', 'betterdocs' ),
398 'icon_class' => 'dashicons dashicons-calendar-alt',
399 'attributes' => array(
400 'data-later' => true,
401 'class' => 'dismiss-btn',
402 ),
403 ),
404 'never_show_again' => array(
405 'label' => __( 'Never show again', 'betterdocs' ),
406 'icon_class' => 'dashicons dashicons-dismiss',
407 'attributes' => array(
408 'data-dismiss' => true,
409 ),
410 ),
411 ),
412 );
413
414 $notices->add(
415 'migration',
416 $_migration_notice,
417 array(
418 'start' => $notices->time(),
419 'recurrence' => false,
420 'dismissible' => true,
421 )
422 );
423 }
424
425 /**
426 *
427 * Opt-In Notice
428 */
429 $allow_tracking = get_option( 'wpins_allow_tracking' );
430 if ( null != $this->insights && ! isset( $allow_tracking['betterdocs'] ) ) {
431 $notices->add(
432 'opt_in',
433 array( $this->insights, 'notice' ),
434 array(
435 'classes' => 'updated put-dismiss-notice',
436 'start' => $notices->time(),
437 'refresh' => BETTERDOCS_VERSION,
438 'dismissible' => true,
439 'do_action' => 'wpdeveloper_notice_clicked_for_betterdocs',
440 'display_if' => ! function_exists( 'betterdocs_pro' ),
441 'screens' => array( 'dashboard' ),
442 )
443 );
444 }
445
446 $summer_campaign_message = '<div class="betterdocs-summer-notice-body"><p style="margin-top: 0; margin-bottom: 0;">🏖️ <strong>Summer Savings:</strong> Build AI-powered Knowledge Bases & FAQs to cut support tickets and improve user experience – now <strong>up to $100 OFF!</strong></p></div>';
447 $_summer_campaign_notice = array(
448 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ),
449 'html' => $summer_campaign_message,
450 'links' => array(
451 'support' => array(
452 'link' => 'https://betterdocs.co/summer2026-admin-notice',
453 'attributes' => array(
454 'target' => '_blank',
455 'class' => 'offer-button',
456 ),
457 'label' => __( 'Upgrade To PRO Now', 'betterdocs' ),
458 ),
459 'maybe_later' => array(
460 'label' => __( 'I’ll Grab It Later', 'betterdocs' ),
461 'attributes' => array(
462 'target' => '_blank',
463 'data-later' => true,
464 'class' => 'dismiss-btn',
465 ),
466 ),
467 ),
468 );
469
470 $notices->add(
471 'summer-campaign-26',
472 $_summer_campaign_notice,
473 array(
474 'start' => $notices->time(),
475 'recurrence' => false,
476 'dismissible' => true,
477 'refresh' => BETTERDOCS_VERSION,
478 'expire' => strtotime( '11:59:59pm June 25, 2026' ),
479 'display_if' => ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ),
480 )
481 );
482
483 self::$cache_bank->create_account( $notices );
484 self::$cache_bank->calculate_deposits( $notices );
485 if ( method_exists( self::$cache_bank, 'clear_notices_in_' ) ) {
486 self::$cache_bank->clear_notices_in_(
487 array(
488 'toplevel_page_betterdocs-dashboard',
489 'admin_page_betterdocs-admin',
490 'betterdocs_page_betterdocs-admin',
491 'betterdocs_page_betterdocs-settings',
492 'betterdocs_page_betterdocs-faq',
493 'betterdocs_page_betterdocs-analytics',
494 'betterdocs_page_betterdocs-glossaries',
495 'betterdocs_page_betterdocs-ai-chatbot',
496 'betterdocs_page_betterdocs-doc-categories',
497 'betterdocs_page_betterdocs-doc-tags',
498 'edit-doc_category',
499 'edit-doc_tag',
500 ),
501 $notices,
502 true
503 );
504 }
505 }
506
507 /**
508 * Resolve the admin dark-mode preference.
509 *
510 * The mode switcher stores the choice in a client cookie (no DB write, shared
511 * across every admin screen). Fall back to the legacy
512 * `betterdocs_settings['dark_mode']` value for installs that set it before this
513 * change and haven't toggled since.
514 *
515 * @return bool
516 */
517 public function is_dark_mode() {
518 if ( isset( $_COOKIE['betterdocs_admin_dark_mode'] ) ) {
519 return '1' === $_COOKIE['betterdocs_admin_dark_mode']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
520 }
521
522 $saved = get_option( 'betterdocs_settings', array() );
523 return ! empty( $saved['dark_mode'] );
524 }
525
526 /**
527 * Whether the knowledge base is genuinely empty (no doc categories and no
528 * non-trash docs). Localized to the admin so the All Docs panel can render a
529 * skeleton shaped like the "No Category Found" empty card on first paint —
530 * instead of a category/docs skeleton it would immediately replace — without
531 * waiting for the REST fetch to reveal the count.
532 *
533 * @return bool
534 */
535 public function kb_is_empty() {
536 $cats = wp_count_terms( array( 'taxonomy' => 'doc_category', 'hide_empty' => false ) );
537 $cats = is_wp_error( $cats ) ? 0 : (int) $cats;
538 if ( $cats > 0 ) {
539 return false;
540 }
541
542 $counts = (array) wp_count_posts( 'docs' );
543 $total = 0;
544 foreach ( array( 'publish', 'future', 'draft', 'pending', 'private' ) as $status ) {
545 $total += isset( $counts[ $status ] ) ? (int) $counts[ $status ] : 0;
546 }
547
548 return 0 === $total;
549 }
550
551 public function body_classes( $classes ) {
552 $dark_mode = $this->is_dark_mode();
553 $current_screen_id = get_current_screen() != null ? str_replace( 'betterdocs_page_', '', str_replace( 'toplevel_page_', '', str_replace( 'admin_page_', '', get_current_screen()->id ) ) ) : '';
554 /**
555 * Filter the list of (prefix-stripped) screen ids that receive the
556 * `betterdocs-admin` body class (and dark-mode class). Pro/add-ons can
557 * register their own React admin pages, e.g. the Knowledge Base page.
558 *
559 * @param string[] $registered_screens Screen ids with the page prefix removed.
560 */
561 $registered_screens = apply_filters( 'betterdocs_admin_screen_slugs', array(
562 'betterdocs-settings',
563 'betterdocs-admin',
564 'betterdocs-dashboard',
565 'betterdocs-analytics',
566 'betterdocs-glossaries',
567 'betterdocs-faq',
568 'betterdocs-doc-categories',
569 'betterdocs-doc-tags',
570 'edit-doc_category',
571 'edit-doc_tag',
572 'edit-knowledge_base',
573 'betterdocs-ai-chatbot',
574 ) );
575
576 if ( in_array( $current_screen_id, $registered_screens ) ) {
577 $classes .= ' betterdocs-admin ';
578 }
579
580 // Dark mode also applies on the Quick Setup wizard, whose self-scoped chrome
581 // keys off `.betterdocs_page_betterdocs-setup.betterdocs-dark-mode`.
582 $dark_screens = array_merge( $registered_screens, array( 'betterdocs-setup' ) );
583 if ( $dark_mode && in_array( $current_screen_id, $dark_screens, true ) ) {
584 $classes .= ' betterdocs-dark-mode ';
585 }
586
587 return $classes;
588 }
589
590 /**
591 * Remove Comments Column From List Table
592 *
593 * @param array $columns
594 *
595 * @return array
596 * @since 1.0.0
597 */
598 public function set_custom_edit_action_columns( $columns ) {
599 unset( $columns['comments'] );
600 $new_columns = array();
601 foreach ( $columns as $key => $value ) {
602 if ( 'date' == $key ) {
603 $new_columns['betterdocs_word_count'] = __( 'Word Count', 'betterdocs' ); // put the tags column before it
604 $new_columns['betterdocs_reaction'] = __( 'Reactions', 'betterdocs' );
605 }
606 $new_columns[ $key ] = $value;
607 }
608
609 return $new_columns;
610 }
611
612 public function manage_custom_columns( $column, $post_id ) {
613 global $wpdb;
614 switch ( $column ) {
615 case 'betterdocs_word_count':
616 $content_without_html_tags = trim( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );
617 preg_match_all( '/<[^>]*>|[\p{L}\p{M}]+/u', $content_without_html_tags, $matches );
618 $total_words = ! empty( $matches[0] ) ? count( $matches[0] ) : count( array() );
619 $word_count = $total_words;
620 echo '<span>' . esc_html( intval( $word_count ) ) . '</span>';
621 break;
622 case 'betterdocs_reaction':
623 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- per-post analytics aggregation rendered in admin list table; cache would mask live reactions.
624 $analytics = $wpdb->get_results(
625 $wpdb->prepare(
626 "SELECT
627 sum(impressions) as totalViews,
628 sum(unique_visit) as totalUniqueViews,
629 sum(happy + sad + normal) as totalReactions,
630 sum(happy) as totalHappy,
631 sum(normal) as totalNormal,
632 sum(sad) as totalSad
633 FROM {$wpdb->prefix}betterdocs_analytics
634 WHERE post_id = %d",
635 $post_id
636 )
637 );
638
639 echo '<ul class="reactions-count">
640 <li>
641 <a title="happy" class="betterdocs-feelings happy" data-feelings="happy" href="#">
642 <svg width="15" height="15" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
643 <path class="st0" d="M10,0.1c-5.4,0-9.9,4.4-9.9,9.8c0,5.4,4.4,9.9,9.8,9.9c5.4,0,9.9-4.4,9.9-9.8C19.9,4.5,15.4,0.1,10,0.1z
644 M13.3,6.4c0.8,0,1.5,0.7,1.5,1.5c0,0.8-0.7,1.5-1.5,1.5c-0.8,0-1.5-0.7-1.5-1.5C11.8,7.1,12.5,6.4,13.3,6.4z M6.7,6.4
645 c0.8,0,1.5,0.7,1.5,1.5c0,0.8-0.7,1.5-1.5,1.5c-0.8,0-1.5-0.7-1.5-1.5C5.2,7.1,5.9,6.4,6.7,6.4z M10,16.1c-2.6,0-4.9-1.6-5.8-4
646 l1.2-0.4c0.7,1.9,2.5,3.2,4.6,3.2s3.9-1.3,4.6-3.2l1.2,0.4C14.9,14.5,12.6,16.1,10,16.1z" />
647 <path class="st1" d="M-6.6-119.7c-7.1,0-12.9,5.8-12.9,12.9s5.8,12.9,12.9,12.9s12.9-5.8,12.9-12.9S0.6-119.7-6.6-119.7z
648 M-2.3-111.4c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2c-1.1,0-2-0.9-2-2C-4.3-110.5-3.4-111.4-2.3-111.4z M-10.9-111.4c1.1,0,2,0.9,2,2
649 c0,1.1-0.9,2-2,2c-1.1,0-2-0.9-2-2C-12.9-110.5-12-111.4-10.9-111.4z M-6.6-98.7c-3.4,0-6.4-2.1-7.6-5.3l1.6-0.6
650 c0.9,2.5,3.3,4.2,6,4.2s5.1-1.7,6-4.2L1-104C-0.1-100.8-3.2-98.7-6.6-98.7z" />
651 </svg>
652 <span>' . esc_html( ( intval( $analytics[0]->totalHappy ) !== null ? intval( $analytics[0]->totalHappy ) : 0 ) ) . '</span>
653 </a>
654 </li>
655 <li>
656 <a title="normal" class="betterdocs-feelings normal" data-feelings="normal" href="#">
657 <svg width="15" height="15" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
658 <path class="st0" d="M10,0.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S15.4,0.2,10,0.2z M6.7,6.5
659 c0.8,0,1.5,0.7,1.5,1.5c0,0.8-0.7,1.5-1.5,1.5C5.9,9.5,5.2,8.9,5.2,8C5.2,7.2,5.9,6.5,6.7,6.5z M14.2,14.3H5.9
660 c-0.3,0-0.6-0.3-0.6-0.6c0-0.3,0.3-0.6,0.6-0.6h8.3c0.3,0,0.6,0.3,0.6,0.6C14.8,14,14.5,14.3,14.2,14.3z M13.3,9.5
661 c-0.8,0-1.5-0.7-1.5-1.5c0-0.8,0.7-1.5,1.5-1.5c0.8,0,1.5,0.7,1.5,1.5C14.8,8.9,14.1,9.5,13.3,9.5z" />
662 </svg>
663 <span>' . esc_html( ( intval( $analytics[0]->totalNormal ) !== null ? intval( $analytics[0]->totalNormal ) : 0 ) ) . '</span>
664 </a>
665 </li>
666 <li>
667 <a title="sad" class="betterdocs-feelings sad" data-feelings="sad" href="#">
668 <svg width="15" height="15" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
669 <circle class="st0" cx="27.5" cy="0.6" r="1.9" />
670 <circle class="st0" cx="36" cy="0.6" r="1.9" />
671 <path class="st1" d="M10,0.3c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S15.4,0.3,10,0.3z M13.3,6.6
672 c0.8,0,1.5,0.7,1.5,1.5c0,0.8-0.7,1.5-1.5,1.5c-0.8,0-1.5-0.7-1.5-1.5C11.8,7.3,12.4,6.6,13.3,6.6z M6.7,6.6c0.8,0,1.5,0.7,1.5,1.5
673 c0,0.8-0.7,1.5-1.5,1.5C5.9,9.6,5.2,9,5.2,8.1C5.2,7.3,5.9,6.6,6.7,6.6z M14.1,15L14.1,15c-0.2,0-0.4-0.1-0.5-0.2
674 c-0.9-1-2.2-1.7-3.7-1.7s-2.8,0.6-3.7,1.7C6.2,14.9,6,15,5.9,15h0c-0.6,0-0.8-0.6-0.5-1.1c1.1-1.3,2.8-2.1,4.6-2.1
675 c1.8,0,3.5,0.8,4.6,2.1C15,14.3,14.7,15,14.1,15z" />
676 </svg>
677 <span>' . esc_html( ( intval( $analytics[0]->totalNormal ) !== null ? intval( $analytics[0]->totalNormal ) : 0 ) ) . '</span>
678 </a>
679 </li>
680 </ul>';
681 break;
682 }
683 }
684
685 /**
686 * Enqueue Assets for Admin ( Styles )
687 *
688 * @param string $hook
689 *
690 * @return void
691 * @since 1.0.0
692 */
693 public function styles( $hook ) {
694 $this->assets->enqueue( 'betterdocs-global', 'admin/css/global.css', array(), 'all' );
695
696 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
697 return;
698 }
699
700 $this->assets->enqueue( 'betterdocs-select2', 'vendor/css/select2.min.css', array(), 'all' );
701 $this->assets->enqueue( 'betterdocs-daterangepicker', 'vendor/css/daterangepicker.css', array(), 'all' );
702 $this->assets->enqueue( 'betterdocs-old', 'admin/css/betterdocs.css', array(), 'all' );
703
704 /**
705 * This scripts enqueued for Dashboard App.
706 */
707 $this->assets->enqueue( 'betterdocs', 'admin/css/dashboard.css', array( 'betterdocs-old' ), '', BETTERDOCS_VERSION );
708 $this->assets->enqueue( 'betterdocs-icons', 'admin/btd-icon/style.css' );
709 }
710
711 /**
712 * Enqueue Assets for Admin ( Scripts )
713 *
714 * @param string $hook
715 *
716 * @return void
717 * @since 1.0.0
718 */
719 public function scripts( $hook ) {
720 // Classic-UI screens that should offer a "Switch to BetterDocs UI"
721 // button: All Docs, FAQ list, FAQ groups, Product FAQ groups,
722 // Doc Categories, Doc Tags. Maps each to the React admin page to
723 // return to; $switch_args carries extra query args (e.g. the FAQ
724 // Builder tab) appended to the React page URL.
725 $switch_page = '';
726 $switch_args = array();
727 if ( 'edit.php' === $hook && 'docs' === get_post_type() ) {
728 $switch_page = 'betterdocs-admin';
729 } elseif ( 'edit.php' === $hook && 'betterdocs_faq' === get_post_type() ) {
730 $switch_page = 'betterdocs-faq';
731 } elseif ( 'edit-tags.php' === $hook ) {
732 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
733 $taxonomy = $screen && ! empty( $screen->taxonomy )
734 ? $screen->taxonomy
735 : ( isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
736 if ( 'betterdocs_faq_category' === $taxonomy ) {
737 $switch_page = 'betterdocs-faq';
738 } elseif ( 'betterdocs_product_faq_category' === $taxonomy ) {
739 // Product FAQ groups live on the FAQ Builder's WooCommerce tab.
740 $switch_page = 'betterdocs-faq';
741 $switch_args = array( 'faq_tab' => 'woocommerce' );
742 } elseif ( 'doc_category' === $taxonomy ) {
743 $switch_page = 'betterdocs-doc-categories';
744 } elseif ( 'doc_tag' === $taxonomy ) {
745 $switch_page = 'betterdocs-doc-tags';
746 }
747 }
748
749 /**
750 * Allow Pro/add-ons to map their own classic-UI taxonomy screens to a
751 * React admin page for the "Switch to BetterDocs UI" button — e.g. the
752 * Knowledge Base taxonomy, which only exists when Pro is active.
753 *
754 * @param string $switch_page React page slug, or '' for no switcher.
755 * @param string $hook Current admin page hook.
756 */
757 $switch_page = apply_filters( 'betterdocs_classic_switch_page', $switch_page, $hook );
758
759 if ( $switch_page ) {
760 $this->assets->enqueue(
761 'betterdocs-switcher',
762 'admin/js/switcher.js',
763 array(
764 'jquery',
765 )
766 );
767
768 $this->assets->localize(
769 'betterdocs-switcher',
770 'betterdocsSwitcher',
771 array(
772 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
773 'page' => $switch_page,
774 'url' => add_query_arg(
775 array_merge( array( 'page' => $switch_page ), $switch_args ),
776 admin_url( 'admin.php' )
777 ),
778 'site_address' => get_bloginfo( 'url' ),
779 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
780 'betterdocs_pro_version' => betterdocs()->pro_version(),
781 )
782 );
783
784 return;
785 }
786
787 wp_enqueue_script( 'wp-editor' ); // enqueue this for yoast related issue
788
789 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
790 return;
791 }
792
793 wp_enqueue_media(); // load early to fix problems with media upload issues on settings for WordPress 6.0.9
794 $this->assets->register( 'betterdocs-admin', 'admin/js/dashboard.js' );
795
796 $saved_settings = get_option( 'betterdocs_settings', false );
797 $dark_mode = $this->is_dark_mode();
798 $this->assets->localize(
799 'betterdocs-admin',
800 'betterdocs_admin',
801 array(
802 'ajaxurl' => admin_url( 'admin-ajax.php' ),
803 'doc_cat_order_nonce' => wp_create_nonce( 'doc_cat_order_nonce' ),
804 'knowledge_base_order_nonce' => wp_create_nonce( 'knowledge_base_order_nonce' ),
805 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- pagination read from URL.
806 'per_page_id' => 'edit_doc_category_per_page',
807 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
808 'dark_mode' => $dark_mode,
809 'kb_is_empty' => $this->kb_is_empty(),
810 'text' => __( 'Copied!', 'betterdocs' ),
811 'test_report' => __( 'Test Report!', 'betterdocs' ),
812 'sending' => __( 'Sending...', 'betterdocs' ),
813 'dir_url' => BETTERDOCS_ABSURL,
814 'rest_url' => esc_url_raw( rest_url() ),
815 'free_version' => betterdocs()->version,
816 'generate_data_url' => get_rest_url( null, '/betterdocs/v1/create-sample-docs' ),
817 'ai_sample_docs' => array(
818 'enabled' => (bool) betterdocs()->settings->get( 'enable_ai_sample_docs', true ),
819 'rest_base' => esc_url_raw( get_rest_url( null, '/betterdocs/v1/sample-docs' ) ),
820 ),
821 'nonce' => wp_create_nonce( 'wp_rest' ),
822 'sync_nonce' => wp_create_nonce( 'ai_chatbot_embed' ),
823 'count_all_docs' => array_sum( (array) wp_count_posts( 'docs' ) ),
824 'count_all_faq' => array_sum( (array) wp_count_posts( 'betterdocs_faq' ) ),
825 'faq_order' => get_option( 'betterdocs_faq_order', 'default' ),
826 'count_new_docs' => $this->get_not_synced_docs_count(),
827 'admin_url' => admin_url(),
828 'ia_preview' => betterdocs()->settings->get( 'ia_enable_preview', false ),
829 'multiple_kb' => betterdocs()->settings->get( 'multiple_kb' ),
830 'previewMode' => betterdocs()->settings->get( 'ia_enable_preview', false ),
831 'dashboard_mode' => get_option( 'dashboard_mode' ),
832 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
833 'betterdocs_pro_version' => betterdocs()->pro_version(),
834 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ),
835 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ),
836 'is_woocommerce_active' => class_exists( 'WooCommerce' ),
837 'total_doc_category_terms' => wp_count_terms( 'doc_category' ),
838 'current_admin_language' => Helper::get_current_admin_language(),
839 'is_multilingual' => Helper::is_multilingual_active(),
840 'languages' => Helper::get_admin_languages(),
841 )
842 );
843
844 // If wp-date (which includes moment.js) is not registered, enqueue your custom moment.js
845 if ( ! wp_script_is( 'wp-date', 'registered' ) ) {
846 $this->assets->enqueue( 'moment', 'vendor/js/moment.min.js', array() );
847 }
848 wp_enqueue_script( 'betterdocs-admin' );
849
850 /**
851 * Duplicate Codes Need to Be Removed From Here Onwards
852 */
853
854 // FAQ Builder Related Localization
855 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/css/faq.css' );
856 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/js/faq.js' );
857
858 // Load the classic editor (TinyMCE + QuickTags) so the FAQ rich-text editor can mount via wp.editor.initialize().
859 if ( function_exists( 'wp_enqueue_editor' ) ) {
860 wp_enqueue_editor();
861 }
862 if ( function_exists( 'wp_enqueue_media' ) ) {
863 wp_enqueue_media();
864 }
865
866 // removing emoji support
867 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
868 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
869
870 // Get settings and remove unnecessary keys
871 $betterdocs_settings = get_option( 'betterdocs_settings', false );
872 if ( is_array( $betterdocs_settings ) && ! current_user_can( 'edit_docs_settings' ) ) {
873 foreach ( Settings::sensitive_api_key_fields() as $sensitive_key ) {
874 unset( $betterdocs_settings[ $sensitive_key ] );
875 }
876 }
877
878 betterdocs()->assets->localize(
879 'betterdocs-admin-faq',
880 'betterdocsFaq',
881 array(
882 'dir_url' => BETTERDOCS_ABSURL,
883 'rest_url' => esc_url_raw( rest_url() ),
884 'free_version' => betterdocs()->version,
885 'nonce' => wp_create_nonce( 'wp_rest' ),
886 'betterdocs_settings' => $betterdocs_settings,
887 )
888 );
889
890 // Glossaries Related Localization
891 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/css/faq.css' );
892
893 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/js/glossaries.js' );
894
895 betterdocs()->assets->localize(
896 'betterdocs-admin-glossaries',
897 'betterdocsGlossary',
898 array(
899 'dir_url' => BETTERDOCS_ABSURL,
900 'rest_url' => esc_url_raw( rest_url() ),
901 'free_version' => betterdocs()->version,
902 'nonce' => wp_create_nonce( 'wp_rest' ),
903 'betterdocs_settings' => $betterdocs_settings,
904 )
905 );
906 }
907
908 /**
909 * All admin pages header
910 *
911 * @return void
912 * @since 1.0.0
913 */
914 public function header( $admin_tab_name ) {
915 $quick_links = array(
916 'switch_view' => sprintf(
917 '<a href="%s" class="betterdocs-button betterdocs-button-secondary">%s</a>',
918 add_query_arg(
919 array(
920 'post_type' => 'docs',
921 'bdocs_view' => 'classic',
922 ),
923 'edit.php'
924 ),
925 __( 'Switch to Classic UI', 'betterdocs' )
926 ),
927 'add_new_doc' => sprintf( '<a href="%s" class="betterdocs-button betterdocs-button-primary">%s</a>', add_query_arg( array( 'post_type' => 'docs' ), 'post-new.php' ), __( 'Add New Doc', 'betterdocs' ) ),
928 );
929
930 $quick_links = apply_filters( 'betterdocs_quick_links', $quick_links );
931
932 betterdocs()->views->get(
933 'admin/header',
934 array(
935 'quick_links' => $quick_links,
936 'active_tab' => $admin_tab_name,
937 )
938 );
939 }
940
941 /**
942 * Register all the menus for BetterDocs
943 *
944 * @return void
945 * @since 1.0.0
946 */
947 public function menus() {
948 $default_args = array(
949 'page_title' => 'BetterDocs',
950 'menu_title' => 'BetterDocs',
951 'capability' => 'edit_docs', // Unified capability
952 'menu_slug' => $this->slug,
953 'callback' => array( $this, 'output' ),
954 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
955 'position' => 5,
956 );
957
958 $_menu_position = 5;
959 global $submenu;
960
961 // Always register both UI endpoints
962 $this->register_modern_ui_fallback();
963
964 foreach ( $this->menu_list() as $key => $value ) {
965 if ( 'betterdocs' === $key ) {
966 $callable = 'add_menu_page';
967 $value = wp_parse_args( $value, $default_args );
968 call_user_func_array( $callable, $value );
969 } else {
970 $is_core_page = strpos( $value['menu_slug'], '?' ) !== false;
971
972 if ( $is_core_page ) {
973 // Add classic UI directly
974 $submenu[ $this->slug ][] = array(
975 $value['menu_title'],
976 $value['capability'],
977 $value['menu_slug'],
978 $value['page_title'],
979 );
980 } else {
981 // Add modern UI through WordPress API
982 add_submenu_page(
983 $this->slug,
984 $value['page_title'],
985 $value['menu_title'],
986 $value['capability'],
987 $value['menu_slug'],
988 $value['callback']
989 );
990 }
991 ++$_menu_position;
992 }
993 }
994 }
995
996 private function register_modern_ui_fallback() {
997 // Add the submenu with valid parent slug
998 add_submenu_page(
999 'betterdocs', // Valid parent slug
1000 __( 'All Docs', 'betterdocs' ),
1001 '', // Empty menu title hides it
1002 'edit_docs',
1003 'betterdocs-admin',
1004 array( $this, 'output' )
1005 );
1006
1007 // Hide the menu item from appearing in the admin sidebar
1008 global $submenu;
1009 if ( isset( $submenu['betterdocs'] ) ) {
1010 foreach ( $submenu['betterdocs'] as $key => $item ) {
1011 if ( 'betterdocs-admin' === $item[2] ) {
1012 unset( $submenu['betterdocs'][ $key ] );
1013 break;
1014 }
1015 }
1016 }
1017 }
1018
1019 /**
1020 * BetterDocs Admin Page Output
1021 *
1022 * @return void
1023 * @since 1.0.0
1024 */
1025 public function output() {
1026 if ( betterdocs()->is_pro_active()
1027 && version_compare( betterdocs()->pro_version(), '3.3.4', '<=' )
1028 && get_current_screen()->id == 'betterdocs_page_betterdocs-analytics' ) {
1029 betterdocs_pro()->views->get( 'admin/analytics-pro' );
1030 } else {
1031 betterdocs()->views->get(
1032 'admin/main',
1033 array(
1034 'admin_ui' => 'dnd',
1035 )
1036 );
1037 }
1038 }
1039
1040 /**
1041 * Menu creator helper
1042 *
1043 * @param string $title
1044 * @param string $slug
1045 * @param string $cap
1046 * @param array $callback
1047 *
1048 * @return array
1049 * @since 2.5.0
1050 */
1051 private function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = array() ) {
1052 return Helper::normalize_menu( $title, $slug, $cap, $callback, $optional );
1053 }
1054
1055 /**
1056 * BetterDocs Menu List
1057 *
1058 * @return array
1059 * @since 1.0.0
1060 */
1061 private function menu_list() {
1062 $parent_slug = array();
1063
1064 $betterdocs_admin_pages = array(
1065 'betterdocs' => array(
1066 'menu_slug' => $this->slug,
1067 'page_title' => 'BetterDocs',
1068 'menu_title' => 'BetterDocs',
1069 'capability' => 'edit_docs',
1070 'callback' => array( $this, 'output' ),
1071 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
1072 'position' => 5,
1073 ),
1074 'dashboard' => $this->normalize_menu(
1075 __( 'Dashboard', 'betterdocs' ),
1076 'betterdocs-dashboard',
1077 'edit_docs',
1078 array(
1079 $this,
1080 'output',
1081 )
1082 ),
1083 'all_docs' => $this->normalize_menu(
1084 __( 'All Docs', 'betterdocs' ),
1085 $this->ui_slug(),
1086 'edit_docs',
1087 array( $this, 'output' ),
1088 $parent_slug
1089 ),
1090 'add_new' => $this->normalize_menu(
1091 __( 'Add New', 'betterdocs' ),
1092 'post-new.php?post_type=docs'
1093 ),
1094 'categories' => $this->normalize_menu(
1095 __( 'Categories', 'betterdocs' ),
1096 'betterdocs-doc-categories',
1097 'manage_doc_terms',
1098 array( $this, 'output' ),
1099 $parent_slug
1100 ),
1101 'tags' => $this->normalize_menu(
1102 __( 'Tags', 'betterdocs' ),
1103 'betterdocs-doc-tags',
1104 'manage_doc_terms',
1105 array( $this, 'output' ),
1106 $parent_slug
1107 ),
1108 'settings' => $this->normalize_menu(
1109 __( 'Settings', 'betterdocs' ),
1110 'betterdocs-settings',
1111 'edit_docs_settings',
1112 array(
1113 $this,
1114 'output',
1115 ),
1116 $parent_slug
1117 ),
1118 'analytics' => $this->normalize_menu(
1119 __( 'Analytics', 'betterdocs' ),
1120 'betterdocs-analytics',
1121 'read_docs_analytics',
1122 array(
1123 $this,
1124 'output',
1125 ),
1126 $parent_slug
1127 ),
1128 'faq' => $this->normalize_menu(
1129 __( 'FAQ Builder', 'betterdocs' ),
1130 'betterdocs-faq',
1131 'read_faq_builder',
1132 array(
1133 $this,
1134 'output',
1135 ),
1136 $parent_slug
1137 ),
1138 );
1139
1140 if ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'enable_glossaries' ) == true ) {
1141 $betterdocs_admin_pages['glossaries'] = $this->normalize_menu(
1142 __( 'Glossaries', 'betterdocs' ),
1143 'betterdocs-glossaries',
1144 'read_docs_analytics',
1145 array(
1146 $this,
1147 'output',
1148 ),
1149 $parent_slug
1150 );
1151 }
1152
1153 if ( ! betterdocs()->is_chatbot_active() ) {
1154 $betterdocs_admin_pages['ai_chatbot'] = $this->normalize_menu(
1155 __( 'AI Chatbot', 'betterdocs' ),
1156 'betterdocs-ai-chatbot',
1157 'edit_docs_settings',
1158 array(
1159 $this,
1160 'output',
1161 ),
1162 $parent_slug
1163 );
1164 }
1165
1166 return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, array( $this, 'output' ), $parent_slug );
1167 }
1168
1169 public function add_custom_classes_to_menu_items() {
1170 global $menu, $submenu;
1171
1172 $menu_items = array(
1173 'betterdocs' => 'betterdocs',
1174 'betterdocs_page_all_docs' => 'betterdocs-all-docs',
1175 'betterdocs_page_add_new' => 'betterdocs-add-new',
1176 'betterdocs-doc-categories' => 'betterdocs-categories',
1177 'betterdocs-doc-tags' => 'betterdocs-tags',
1178 'betterdocs-settings' => 'betterdocs-settings',
1179 'betterdocs-analytics' => 'betterdocs-analytics',
1180 'betterdocs-faq' => 'betterdocs-faq',
1181 'betterdocs-glossaries' => 'betterdocs-glossaries',
1182 'betterdocs-ai-chatbot' => 'betterdocs-ai-chatbot',
1183 'edit-tags.php?taxonomy=knowledge_base&post_type=docs' => 'betterdocs-multiplekb',
1184 );
1185
1186 foreach ( $menu as &$item ) {
1187 if ( isset( $menu_items[ $item[2] ] ) ) {
1188 if ( ! isset( $item[4] ) ) {
1189 $item[4] = '';
1190 }
1191 $item[4] .= '' . $menu_items[ $item[2] ];
1192 }
1193 }
1194
1195 foreach ( $submenu as &$submenu_items ) {
1196 foreach ( $submenu_items as &$sub_item ) {
1197 if ( isset( $menu_items[ $sub_item[2] ] ) ) {
1198 if ( ! isset( $sub_item[4] ) ) {
1199 $sub_item[4] = '';
1200 }
1201 $sub_item[4] .= '' . $menu_items[ $sub_item[2] ];
1202 }
1203 }
1204 }
1205 }
1206
1207 public function quick_setup_menu( $menus ) {
1208 $betterdocs_settings = get_option( 'betterdocs_settings' );
1209 if ( $betterdocs_settings ) {
1210 return $menus;
1211 } else {
1212 $menus['quick_setup'] = $this->normalize_menu(
1213 __( 'Quick Setup', 'betterdocs' ),
1214 'betterdocs-setup',
1215 'delete_users',
1216 array(
1217 $this->container->get( SetupWizard::class ),
1218 'views',
1219 )
1220 );
1221 }
1222
1223 return $menus;
1224 }
1225
1226 public function insert_plugin_links( $links ) {
1227 $links[] = '<a href="admin.php?page=betterdocs-settings">' . __( 'Settings', 'betterdocs' ) . '</a>';
1228
1229 if ( ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) {
1230 $links[] = '<a href="https://betterdocs.co/upgrade-to-pro-plugins-wp" target="_blank" style="color: #000; font-weight: bold;">' . __( 'Upgrade to Pro', 'betterdocs' ) . '</a>';
1231 }
1232
1233 return $links;
1234 }
1235
1236 public function toolbar_menu( $admin_bar ) {
1237 if ( ! is_admin() || ! is_admin_bar_showing() ) {
1238 return;
1239 }
1240
1241 // Show only when the user is a member of this site, or they're a super admin.
1242 if ( ! is_user_member_of_blog() && ! is_super_admin() ) {
1243 return;
1244 }
1245
1246 $docs_url = '';
1247 $encyclopedia_url = '';
1248
1249 if ( $this->settings->get( 'builtin_doc_page' ) ) {
1250 $docs_url = get_post_type_archive_link( 'docs' );
1251 } elseif ( intval( $docs_page = $this->settings->get( 'docs_page' ) ) ) {
1252 $docs_url = ! empty( $docs_page ) ? get_page_link( $docs_page ) : false;
1253 }
1254
1255 if ( ! $docs_url ) {
1256 return;
1257 }
1258
1259 $slug = $this->settings->get( 'encyclopedia_root_slug' );
1260
1261 global $wp_rewrite;
1262 if ( $wp_rewrite->using_index_permalinks() ) {
1263 $slug = $wp_rewrite->index . '/' . $slug;
1264 }
1265
1266 $encyclopedia_url = home_url( $slug );
1267
1268 $admin_bar->add_node(
1269 array(
1270 'parent' => 'site-name',
1271 'id' => 'view-docs',
1272 'title' => __( 'Visit Documentation', 'betterdocs' ),
1273 'href' => $docs_url,
1274 )
1275 );
1276
1277 $is_enable_encyclopedia = betterdocs()->settings->get( 'enable_encyclopedia' );
1278
1279 if ( $is_enable_encyclopedia && betterdocs()->is_pro_active() ) {
1280 $admin_bar->add_node(
1281 array(
1282 'parent' => 'site-name',
1283 'id' => 'view-encyclopedia',
1284 'title' => __( 'Visit Encyclopedia', 'betterdocs' ),
1285 'href' => $encyclopedia_url,
1286 )
1287 );
1288 }
1289 }
1290
1291 /**
1292 * Save last visited admin ui
1293 *
1294 * @since 3.0.1
1295 */
1296 public function save_admin_page() {
1297 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1298 $post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
1299 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1300 $bdocs_view = isset( $_GET['bdocs_view'] ) ? sanitize_text_field( wp_unslash( $_GET['bdocs_view'] ) ) : '';
1301 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1302 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
1303
1304 if ( 'docs' === $post_type && 'classic' === $bdocs_view ) {
1305 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'classic_ui' );
1306 } elseif ( 'betterdocs-admin' === $page ) {
1307 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'modern_ui' );
1308 }
1309 }
1310
1311 /**
1312 * Return last visited admin ui slug
1313 *
1314 * @return string
1315 * @since 3.0.1
1316 */
1317 public function ui_slug() {
1318 $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1319 $docs_exist = get_posts(
1320 array(
1321 'post_type' => 'docs',
1322 'post_status' => 'any',
1323 'numberposts' => 1,
1324 )
1325 );
1326
1327 return ( 'modern_ui' === $last_visited || empty( $docs_exist ) )
1328 ? 'betterdocs-admin'
1329 : 'edit.php?post_type=docs&bdocs_view=classic';
1330 }
1331
1332 /**
1333 * Resets a duplicate submenu in WordPress if the parent main menu and the first submenu permalink are not the same.
1334 *
1335 * @return string
1336 * @since 3.0.1
1337 */
1338 public function reset_submenu() {
1339 global $submenu;
1340
1341 $docs = get_posts( array( 'post_type' => 'docs' ) );
1342 if ( count( $docs ) == 0 ) {
1343 return;
1344 }
1345
1346 $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1347
1348 if ( 'classic_ui' === $last_visited && isset( $submenu['betterdocs-admin'] ) && in_array( 'betterdocs-admin', $submenu['betterdocs-admin'][0] ) ) {
1349 unset( $submenu['betterdocs-admin'][0] );
1350 $submenu['betterdocs-admin'] = array_values( $submenu['betterdocs-admin'] );
1351 }
1352 }
1353
1354 /**
1355 * Initialize Black Friday Pointer
1356 *
1357 * @return void
1358 * @since 3.7.0
1359 */
1360 private function init_black_friday_pointer() {
1361 // Only initialize if conditions are met
1362 if ( NoticePointers::should_display_notice() ) {
1363 new NoticePointers();
1364 }
1365 }
1366
1367 /**
1368 * AJAX handler for dismissing Black Friday pointer
1369 *
1370 * @return void
1371 * @since 3.7.0
1372 */
1373 public function ajax_dismiss_black_friday_pointer() {
1374 // Verify nonce
1375 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
1376 if ( ! wp_verify_nonce( $nonce, 'betterdocs_dismiss_pointer' ) ) {
1377 wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'betterdocs' ) ) );
1378 }
1379
1380 // Check if user has permission
1381 if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'edit_docs' ) ) {
1382 wp_send_json_error( array( 'message' => __( 'Permission denied', 'betterdocs' ) ) );
1383 }
1384
1385 // Get the introduction key
1386 $introduction_key = isset( $_POST['introduction_key'] ) ? sanitize_text_field( wp_unslash( $_POST['introduction_key'] ) ) : '';
1387
1388 if ( empty( $introduction_key ) ) {
1389 wp_send_json_error( array( 'message' => __( 'Invalid introduction key', 'betterdocs' ) ) );
1390 }
1391
1392 // Set the introduction as viewed
1393 NoticePointers::set_introduction_viewed( $introduction_key );
1394
1395 // Clear the priority option so other plugins can set their priority
1396 delete_option( '_wpdeveloper_plugin_pointer_priority' );
1397
1398 wp_send_json_success( array( 'message' => __( 'Pointer dismissed successfully', 'betterdocs' ) ) );
1399 }
1400
1401 /**
1402 * Get count of docs that are not yet synced
1403 * Similar to Helper::get_not_synced_post_ids() in betterdocs-ai-chatbot plugin
1404 *
1405 * @return int
1406 * @since 3.7.0
1407 */
1408 private function get_not_synced_docs_count() {
1409 $new_post_ids = get_option( 'saved_docs_post_ids', array() );
1410 $error_posts_data = get_option( 'betterdocs_ai_chatbot_error_posts', array() );
1411
1412 // Extract post IDs from error posts (handle both old and new formats)
1413 $error_post_ids = array();
1414 if ( is_array( $error_posts_data ) && ! empty( $error_posts_data ) ) {
1415 foreach ( $error_posts_data as $key => $value ) {
1416 if ( is_numeric( $key ) && is_numeric( $value ) ) {
1417 // Old format: numeric key with post ID as value
1418 $error_post_ids[] = $value;
1419 } elseif ( is_numeric( $key ) && is_array( $value ) && isset( $value['post_id'] ) ) {
1420 // New format: post ID as key with structured data
1421 $error_post_ids[] = $key;
1422 } elseif ( is_numeric( $key ) ) {
1423 // New format: post ID as key
1424 $error_post_ids[] = $key;
1425 }
1426 }
1427 }
1428
1429 // Merge both arrays and remove duplicates, then count
1430 return count( array_unique( array_merge( $new_post_ids, $error_post_ids ) ) );
1431 }
1432 }
1433