PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.2
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.2, at includes/Core/Admin.php

1,453 lines 49.5 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-api-docs',
497 'betterdocs_page_betterdocs-doc-categories',
498 'betterdocs_page_betterdocs-doc-tags',
499 'edit-doc_category',
500 'edit-doc_tag',
501 ),
502 $notices,
503 true
504 );
505 }
506 }
507
508 /**
509 * Resolve the admin dark-mode preference.
510 *
511 * The mode switcher stores the choice in a client cookie (no DB write, shared
512 * across every admin screen). Fall back to the legacy
513 * `betterdocs_settings['dark_mode']` value for installs that set it before this
514 * change and haven't toggled since.
515 *
516 * @return bool
517 */
518 public function is_dark_mode() {
519 if ( isset( $_COOKIE['betterdocs_admin_dark_mode'] ) ) {
520 return '1' === $_COOKIE['betterdocs_admin_dark_mode']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
521 }
522
523 $saved = get_option( 'betterdocs_settings', array() );
524 return ! empty( $saved['dark_mode'] );
525 }
526
527 /**
528 * Whether the knowledge base is genuinely empty (no doc categories and no
529 * non-trash docs). Localized to the admin so the All Docs panel can render a
530 * skeleton shaped like the "No Category Found" empty card on first paint —
531 * instead of a category/docs skeleton it would immediately replace — without
532 * waiting for the REST fetch to reveal the count.
533 *
534 * @return bool
535 */
536 public function kb_is_empty() {
537 $cats = wp_count_terms( array( 'taxonomy' => 'doc_category', 'hide_empty' => false ) );
538 $cats = is_wp_error( $cats ) ? 0 : (int) $cats;
539 if ( $cats > 0 ) {
540 return false;
541 }
542
543 $counts = (array) wp_count_posts( 'docs' );
544 $total = 0;
545 foreach ( array( 'publish', 'future', 'draft', 'pending', 'private' ) as $status ) {
546 $total += isset( $counts[ $status ] ) ? (int) $counts[ $status ] : 0;
547 }
548
549 return 0 === $total;
550 }
551
552 public function body_classes( $classes ) {
553 $dark_mode = $this->is_dark_mode();
554 $current_screen_id = get_current_screen() != null ? str_replace( 'betterdocs_page_', '', str_replace( 'toplevel_page_', '', str_replace( 'admin_page_', '', get_current_screen()->id ) ) ) : '';
555 /**
556 * Filter the list of (prefix-stripped) screen ids that receive the
557 * `betterdocs-admin` body class (and dark-mode class). Pro/add-ons can
558 * register their own React admin pages, e.g. the Knowledge Base page.
559 *
560 * @param string[] $registered_screens Screen ids with the page prefix removed.
561 */
562 $registered_screens = apply_filters( 'betterdocs_admin_screen_slugs', array(
563 'betterdocs-settings',
564 'betterdocs-admin',
565 'betterdocs-dashboard',
566 'betterdocs-analytics',
567 'betterdocs-glossaries',
568 'betterdocs-faq',
569 'betterdocs-doc-categories',
570 'betterdocs-doc-tags',
571 'edit-doc_category',
572 'edit-doc_tag',
573 'edit-knowledge_base',
574 'betterdocs-ai-chatbot',
575 'betterdocs-api-docs',
576 ) );
577
578 if ( in_array( $current_screen_id, $registered_screens ) ) {
579 $classes .= ' betterdocs-admin ';
580 }
581
582 // Dark mode also applies on the Quick Setup wizard, whose self-scoped chrome
583 // keys off `.betterdocs_page_betterdocs-setup.betterdocs-dark-mode`.
584 $dark_screens = array_merge( $registered_screens, array( 'betterdocs-setup' ) );
585 if ( $dark_mode && in_array( $current_screen_id, $dark_screens, true ) ) {
586 $classes .= ' betterdocs-dark-mode ';
587 }
588
589 return $classes;
590 }
591
592 /**
593 * Remove Comments Column From List Table
594 *
595 * @param array $columns
596 *
597 * @return array
598 * @since 1.0.0
599 */
600 public function set_custom_edit_action_columns( $columns ) {
601 unset( $columns['comments'] );
602 $new_columns = array();
603 foreach ( $columns as $key => $value ) {
604 if ( 'date' == $key ) {
605 $new_columns['betterdocs_word_count'] = __( 'Word Count', 'betterdocs' ); // put the tags column before it
606 $new_columns['betterdocs_reaction'] = __( 'Reactions', 'betterdocs' );
607 }
608 $new_columns[ $key ] = $value;
609 }
610
611 return $new_columns;
612 }
613
614 public function manage_custom_columns( $column, $post_id ) {
615 global $wpdb;
616 switch ( $column ) {
617 case 'betterdocs_word_count':
618 $content_without_html_tags = trim( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );
619 preg_match_all( '/<[^>]*>|[\p{L}\p{M}]+/u', $content_without_html_tags, $matches );
620 $total_words = ! empty( $matches[0] ) ? count( $matches[0] ) : count( array() );
621 $word_count = $total_words;
622 echo '<span>' . esc_html( intval( $word_count ) ) . '</span>';
623 break;
624 case 'betterdocs_reaction':
625 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- per-post analytics aggregation rendered in admin list table; cache would mask live reactions.
626 $analytics = $wpdb->get_results(
627 $wpdb->prepare(
628 "SELECT
629 sum(impressions) as totalViews,
630 sum(unique_visit) as totalUniqueViews,
631 sum(happy + sad + normal) as totalReactions,
632 sum(happy) as totalHappy,
633 sum(normal) as totalNormal,
634 sum(sad) as totalSad
635 FROM {$wpdb->prefix}betterdocs_analytics
636 WHERE post_id = %d",
637 $post_id
638 )
639 );
640
641 echo '<ul class="reactions-count">
642 <li>
643 <a title="happy" class="betterdocs-feelings happy" data-feelings="happy" href="#">
644 <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">
645 <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
646 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
647 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
648 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" />
649 <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
650 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
651 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
652 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" />
653 </svg>
654 <span>' . esc_html( ( intval( $analytics[0]->totalHappy ) !== null ? intval( $analytics[0]->totalHappy ) : 0 ) ) . '</span>
655 </a>
656 </li>
657 <li>
658 <a title="normal" class="betterdocs-feelings normal" data-feelings="normal" href="#">
659 <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">
660 <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
661 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
662 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
663 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" />
664 </svg>
665 <span>' . esc_html( ( intval( $analytics[0]->totalNormal ) !== null ? intval( $analytics[0]->totalNormal ) : 0 ) ) . '</span>
666 </a>
667 </li>
668 <li>
669 <a title="sad" class="betterdocs-feelings sad" data-feelings="sad" href="#">
670 <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">
671 <circle class="st0" cx="27.5" cy="0.6" r="1.9" />
672 <circle class="st0" cx="36" cy="0.6" r="1.9" />
673 <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
674 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
675 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
676 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
677 c1.8,0,3.5,0.8,4.6,2.1C15,14.3,14.7,15,14.1,15z" />
678 </svg>
679 <span>' . esc_html( ( intval( $analytics[0]->totalNormal ) !== null ? intval( $analytics[0]->totalNormal ) : 0 ) ) . '</span>
680 </a>
681 </li>
682 </ul>';
683 break;
684 }
685 }
686
687 /**
688 * Enqueue Assets for Admin ( Styles )
689 *
690 * @param string $hook
691 *
692 * @return void
693 * @since 1.0.0
694 */
695 public function styles( $hook ) {
696 $this->assets->enqueue( 'betterdocs-global', 'admin/css/global.css', array(), 'all' );
697
698 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
699 return;
700 }
701
702 $this->assets->enqueue( 'betterdocs-select2', 'vendor/css/select2.min.css', array(), 'all' );
703 $this->assets->enqueue( 'betterdocs-daterangepicker', 'vendor/css/daterangepicker.css', array(), 'all' );
704 $this->assets->enqueue( 'betterdocs-old', 'admin/css/betterdocs.css', array(), 'all' );
705
706 /**
707 * This scripts enqueued for Dashboard App.
708 */
709 $this->assets->enqueue( 'betterdocs', 'admin/css/dashboard.css', array( 'betterdocs-old' ), '', BETTERDOCS_VERSION );
710 $this->assets->enqueue( 'betterdocs-icons', 'admin/btd-icon/style.css' );
711 }
712
713 /**
714 * Enqueue Assets for Admin ( Scripts )
715 *
716 * @param string $hook
717 *
718 * @return void
719 * @since 1.0.0
720 */
721 public function scripts( $hook ) {
722 // Classic-UI screens that should offer a "Switch to BetterDocs UI"
723 // button: All Docs, FAQ list, FAQ groups, Product FAQ groups,
724 // Doc Categories, Doc Tags. Maps each to the React admin page to
725 // return to; $switch_args carries extra query args (e.g. the FAQ
726 // Builder tab) appended to the React page URL.
727 $switch_page = '';
728 $switch_args = array();
729 if ( 'edit.php' === $hook && 'docs' === get_post_type() ) {
730 $switch_page = 'betterdocs-admin';
731 } elseif ( 'edit.php' === $hook && 'betterdocs_faq' === get_post_type() ) {
732 $switch_page = 'betterdocs-faq';
733 } elseif ( 'edit-tags.php' === $hook ) {
734 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
735 $taxonomy = $screen && ! empty( $screen->taxonomy )
736 ? $screen->taxonomy
737 : ( isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
738 if ( 'betterdocs_faq_category' === $taxonomy ) {
739 $switch_page = 'betterdocs-faq';
740 } elseif ( 'betterdocs_product_faq_category' === $taxonomy ) {
741 // Product FAQ groups live on the FAQ Builder's WooCommerce tab.
742 $switch_page = 'betterdocs-faq';
743 $switch_args = array( 'faq_tab' => 'woocommerce' );
744 } elseif ( 'doc_category' === $taxonomy ) {
745 $switch_page = 'betterdocs-doc-categories';
746 } elseif ( 'doc_tag' === $taxonomy ) {
747 $switch_page = 'betterdocs-doc-tags';
748 }
749 }
750
751 /**
752 * Allow Pro/add-ons to map their own classic-UI taxonomy screens to a
753 * React admin page for the "Switch to BetterDocs UI" button — e.g. the
754 * Knowledge Base taxonomy, which only exists when Pro is active.
755 *
756 * @param string $switch_page React page slug, or '' for no switcher.
757 * @param string $hook Current admin page hook.
758 */
759 $switch_page = apply_filters( 'betterdocs_classic_switch_page', $switch_page, $hook );
760
761 if ( $switch_page ) {
762 $this->assets->enqueue(
763 'betterdocs-switcher',
764 'admin/js/switcher.js',
765 array(
766 'jquery',
767 )
768 );
769
770 $this->assets->localize(
771 'betterdocs-switcher',
772 'betterdocsSwitcher',
773 array(
774 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
775 'page' => $switch_page,
776 'url' => add_query_arg(
777 array_merge( array( 'page' => $switch_page ), $switch_args ),
778 admin_url( 'admin.php' )
779 ),
780 'site_address' => get_bloginfo( 'url' ),
781 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
782 'betterdocs_pro_version' => betterdocs()->pro_version(),
783 )
784 );
785
786 return;
787 }
788
789 wp_enqueue_script( 'wp-editor' ); // enqueue this for yoast related issue
790
791 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
792 return;
793 }
794
795 wp_enqueue_media(); // load early to fix problems with media upload issues on settings for WordPress 6.0.9
796 $this->assets->register( 'betterdocs-admin', 'admin/js/dashboard.js' );
797
798 $saved_settings = get_option( 'betterdocs_settings', false );
799 $dark_mode = $this->is_dark_mode();
800 $this->assets->localize(
801 'betterdocs-admin',
802 'betterdocs_admin',
803 array(
804 'ajaxurl' => admin_url( 'admin-ajax.php' ),
805 'doc_cat_order_nonce' => wp_create_nonce( 'doc_cat_order_nonce' ),
806 'knowledge_base_order_nonce' => wp_create_nonce( 'knowledge_base_order_nonce' ),
807 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- pagination read from URL.
808 'per_page_id' => 'edit_doc_category_per_page',
809 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
810 'dark_mode' => $dark_mode,
811 'kb_is_empty' => $this->kb_is_empty(),
812 'text' => __( 'Copied!', 'betterdocs' ),
813 'test_report' => __( 'Test Report!', 'betterdocs' ),
814 'sending' => __( 'Sending...', 'betterdocs' ),
815 'dir_url' => BETTERDOCS_ABSURL,
816 'rest_url' => esc_url_raw( rest_url() ),
817 'free_version' => betterdocs()->version,
818 'generate_data_url' => get_rest_url( null, '/betterdocs/v1/create-sample-docs' ),
819 'ai_sample_docs' => array(
820 'enabled' => (bool) betterdocs()->settings->get( 'enable_ai_sample_docs', true ),
821 'rest_base' => esc_url_raw( get_rest_url( null, '/betterdocs/v1/sample-docs' ) ),
822 ),
823 'nonce' => wp_create_nonce( 'wp_rest' ),
824 'sync_nonce' => wp_create_nonce( 'ai_chatbot_embed' ),
825 'count_all_docs' => array_sum( (array) wp_count_posts( 'docs' ) ),
826 'count_all_faq' => array_sum( (array) wp_count_posts( 'betterdocs_faq' ) ),
827 'faq_order' => get_option( 'betterdocs_faq_order', 'default' ),
828 'count_new_docs' => $this->get_not_synced_docs_count(),
829 'admin_url' => admin_url(),
830 'ia_preview' => betterdocs()->settings->get( 'ia_enable_preview', false ),
831 'multiple_kb' => betterdocs()->settings->get( 'multiple_kb' ),
832 'previewMode' => betterdocs()->settings->get( 'ia_enable_preview', false ),
833 'dashboard_mode' => get_option( 'dashboard_mode' ),
834 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
835 'betterdocs_pro_version' => betterdocs()->pro_version(),
836 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ),
837 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ),
838 'api_docs_teaser' => betterdocs()->show_api_docs_teaser(),
839 'is_woocommerce_active' => class_exists( 'WooCommerce' ),
840 'total_doc_category_terms' => wp_count_terms( 'doc_category' ),
841 'current_admin_language' => Helper::get_current_admin_language(),
842 'is_multilingual' => Helper::is_multilingual_active(),
843 'languages' => Helper::get_admin_languages(),
844 )
845 );
846
847 // If wp-date (which includes moment.js) is not registered, enqueue your custom moment.js
848 if ( ! wp_script_is( 'wp-date', 'registered' ) ) {
849 $this->assets->enqueue( 'moment', 'vendor/js/moment.min.js', array() );
850 }
851 wp_enqueue_script( 'betterdocs-admin' );
852
853 /**
854 * Duplicate Codes Need to Be Removed From Here Onwards
855 */
856
857 // FAQ Builder Related Localization
858 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/css/faq.css' );
859 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/js/faq.js' );
860
861 // Load the classic editor (TinyMCE + QuickTags) so the FAQ rich-text editor can mount via wp.editor.initialize().
862 if ( function_exists( 'wp_enqueue_editor' ) ) {
863 wp_enqueue_editor();
864 }
865 if ( function_exists( 'wp_enqueue_media' ) ) {
866 wp_enqueue_media();
867 }
868
869 // removing emoji support
870 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
871 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
872
873 // Get settings and remove unnecessary keys
874 $betterdocs_settings = get_option( 'betterdocs_settings', false );
875 if ( is_array( $betterdocs_settings ) && ! current_user_can( 'edit_docs_settings' ) ) {
876 foreach ( Settings::sensitive_api_key_fields() as $sensitive_key ) {
877 unset( $betterdocs_settings[ $sensitive_key ] );
878 }
879 }
880
881 betterdocs()->assets->localize(
882 'betterdocs-admin-faq',
883 'betterdocsFaq',
884 array(
885 'dir_url' => BETTERDOCS_ABSURL,
886 'rest_url' => esc_url_raw( rest_url() ),
887 'free_version' => betterdocs()->version,
888 'nonce' => wp_create_nonce( 'wp_rest' ),
889 'betterdocs_settings' => $betterdocs_settings,
890 )
891 );
892
893 // Glossaries Related Localization
894 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/css/faq.css' );
895
896 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/js/glossaries.js' );
897
898 betterdocs()->assets->localize(
899 'betterdocs-admin-glossaries',
900 'betterdocsGlossary',
901 array(
902 'dir_url' => BETTERDOCS_ABSURL,
903 'rest_url' => esc_url_raw( rest_url() ),
904 'free_version' => betterdocs()->version,
905 'nonce' => wp_create_nonce( 'wp_rest' ),
906 'betterdocs_settings' => $betterdocs_settings,
907 )
908 );
909 }
910
911 /**
912 * All admin pages header
913 *
914 * @return void
915 * @since 1.0.0
916 */
917 public function header( $admin_tab_name ) {
918 $quick_links = array(
919 'switch_view' => sprintf(
920 '<a href="%s" class="betterdocs-button betterdocs-button-secondary">%s</a>',
921 add_query_arg(
922 array(
923 'post_type' => 'docs',
924 'bdocs_view' => 'classic',
925 ),
926 'edit.php'
927 ),
928 __( 'Switch to Classic UI', 'betterdocs' )
929 ),
930 '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' ) ),
931 );
932
933 $quick_links = apply_filters( 'betterdocs_quick_links', $quick_links );
934
935 betterdocs()->views->get(
936 'admin/header',
937 array(
938 'quick_links' => $quick_links,
939 'active_tab' => $admin_tab_name,
940 )
941 );
942 }
943
944 /**
945 * Register all the menus for BetterDocs
946 *
947 * @return void
948 * @since 1.0.0
949 */
950 public function menus() {
951 $default_args = array(
952 'page_title' => 'BetterDocs',
953 'menu_title' => 'BetterDocs',
954 'capability' => 'edit_docs', // Unified capability
955 'menu_slug' => $this->slug,
956 'callback' => array( $this, 'output' ),
957 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
958 'position' => 5,
959 );
960
961 $_menu_position = 5;
962 global $submenu;
963
964 // Always register both UI endpoints
965 $this->register_modern_ui_fallback();
966
967 foreach ( $this->menu_list() as $key => $value ) {
968 if ( 'betterdocs' === $key ) {
969 $callable = 'add_menu_page';
970 $value = wp_parse_args( $value, $default_args );
971 call_user_func_array( $callable, $value );
972 } else {
973 $is_core_page = strpos( $value['menu_slug'], '?' ) !== false;
974
975 if ( $is_core_page ) {
976 // Add classic UI directly
977 $submenu[ $this->slug ][] = array(
978 $value['menu_title'],
979 $value['capability'],
980 $value['menu_slug'],
981 $value['page_title'],
982 );
983 } else {
984 // Add modern UI through WordPress API
985 add_submenu_page(
986 $this->slug,
987 $value['page_title'],
988 $value['menu_title'],
989 $value['capability'],
990 $value['menu_slug'],
991 $value['callback']
992 );
993 }
994 ++$_menu_position;
995 }
996 }
997 }
998
999 private function register_modern_ui_fallback() {
1000 // Add the submenu with valid parent slug
1001 add_submenu_page(
1002 'betterdocs', // Valid parent slug
1003 __( 'All Docs', 'betterdocs' ),
1004 '', // Empty menu title hides it
1005 'edit_docs',
1006 'betterdocs-admin',
1007 array( $this, 'output' )
1008 );
1009
1010 // Hide the menu item from appearing in the admin sidebar
1011 global $submenu;
1012 if ( isset( $submenu['betterdocs'] ) ) {
1013 foreach ( $submenu['betterdocs'] as $key => $item ) {
1014 if ( 'betterdocs-admin' === $item[2] ) {
1015 unset( $submenu['betterdocs'][ $key ] );
1016 break;
1017 }
1018 }
1019 }
1020 }
1021
1022 /**
1023 * BetterDocs Admin Page Output
1024 *
1025 * @return void
1026 * @since 1.0.0
1027 */
1028 public function output() {
1029 if ( betterdocs()->is_pro_active()
1030 && version_compare( betterdocs()->pro_version(), '3.3.4', '<=' )
1031 && get_current_screen()->id == 'betterdocs_page_betterdocs-analytics' ) {
1032 betterdocs_pro()->views->get( 'admin/analytics-pro' );
1033 } else {
1034 betterdocs()->views->get(
1035 'admin/main',
1036 array(
1037 'admin_ui' => 'dnd',
1038 )
1039 );
1040 }
1041 }
1042
1043 /**
1044 * Menu creator helper
1045 *
1046 * @param string $title
1047 * @param string $slug
1048 * @param string $cap
1049 * @param array $callback
1050 *
1051 * @return array
1052 * @since 2.5.0
1053 */
1054 private function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = array() ) {
1055 return Helper::normalize_menu( $title, $slug, $cap, $callback, $optional );
1056 }
1057
1058 /**
1059 * BetterDocs Menu List
1060 *
1061 * @return array
1062 * @since 1.0.0
1063 */
1064 private function menu_list() {
1065 $parent_slug = array();
1066
1067 $betterdocs_admin_pages = array(
1068 'betterdocs' => array(
1069 'menu_slug' => $this->slug,
1070 'page_title' => 'BetterDocs',
1071 'menu_title' => 'BetterDocs',
1072 'capability' => 'edit_docs',
1073 'callback' => array( $this, 'output' ),
1074 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
1075 'position' => 5,
1076 ),
1077 'dashboard' => $this->normalize_menu(
1078 __( 'Dashboard', 'betterdocs' ),
1079 'betterdocs-dashboard',
1080 'edit_docs',
1081 array(
1082 $this,
1083 'output',
1084 )
1085 ),
1086 'all_docs' => $this->normalize_menu(
1087 __( 'All Docs', 'betterdocs' ),
1088 $this->ui_slug(),
1089 'edit_docs',
1090 array( $this, 'output' ),
1091 $parent_slug
1092 ),
1093 'add_new' => $this->normalize_menu(
1094 __( 'Add New', 'betterdocs' ),
1095 'post-new.php?post_type=docs'
1096 ),
1097 'categories' => $this->normalize_menu(
1098 __( 'Categories', 'betterdocs' ),
1099 'betterdocs-doc-categories',
1100 'manage_doc_terms',
1101 array( $this, 'output' ),
1102 $parent_slug
1103 ),
1104 'tags' => $this->normalize_menu(
1105 __( 'Tags', 'betterdocs' ),
1106 'betterdocs-doc-tags',
1107 'manage_doc_terms',
1108 array( $this, 'output' ),
1109 $parent_slug
1110 ),
1111 'settings' => $this->normalize_menu(
1112 __( 'Settings', 'betterdocs' ),
1113 'betterdocs-settings',
1114 'edit_docs_settings',
1115 array(
1116 $this,
1117 'output',
1118 ),
1119 $parent_slug
1120 ),
1121 'analytics' => $this->normalize_menu(
1122 __( 'Analytics', 'betterdocs' ),
1123 'betterdocs-analytics',
1124 'read_docs_analytics',
1125 array(
1126 $this,
1127 'output',
1128 ),
1129 $parent_slug
1130 ),
1131 'faq' => $this->normalize_menu(
1132 __( 'FAQ Builder', 'betterdocs' ),
1133 'betterdocs-faq',
1134 'read_faq_builder',
1135 array(
1136 $this,
1137 'output',
1138 ),
1139 $parent_slug
1140 ),
1141 );
1142
1143 if ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'enable_glossaries' ) == true ) {
1144 $betterdocs_admin_pages['glossaries'] = $this->normalize_menu(
1145 __( 'Glossaries', 'betterdocs' ),
1146 'betterdocs-glossaries',
1147 'read_docs_analytics',
1148 array(
1149 $this,
1150 'output',
1151 ),
1152 $parent_slug
1153 );
1154 }
1155
1156 // API Docs ships in Pro, which overwrites this same key in place — declaring
1157 // the slot here is what keeps the item in this position. Without Pro it
1158 // holds Free's locked teaser instead.
1159 if ( betterdocs()->show_api_docs_teaser() || betterdocs()->has_api_docs() ) {
1160 $betterdocs_admin_pages['api_docs'] = $this->normalize_menu(
1161 __( 'API Docs', 'betterdocs' ),
1162 'betterdocs-api-docs',
1163 apply_filters( 'betterdocs_api_ref_capability', 'manage_options' ),
1164 array(
1165 $this,
1166 'output',
1167 ),
1168 $parent_slug
1169 );
1170 }
1171
1172 if ( ! betterdocs()->is_chatbot_active() ) {
1173 $betterdocs_admin_pages['ai_chatbot'] = $this->normalize_menu(
1174 __( 'AI Chatbot', 'betterdocs' ),
1175 'betterdocs-ai-chatbot',
1176 'edit_docs_settings',
1177 array(
1178 $this,
1179 'output',
1180 ),
1181 $parent_slug
1182 );
1183 }
1184
1185 return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, array( $this, 'output' ), $parent_slug );
1186 }
1187
1188 public function add_custom_classes_to_menu_items() {
1189 global $menu, $submenu;
1190
1191 $menu_items = array(
1192 'betterdocs' => 'betterdocs',
1193 'betterdocs_page_all_docs' => 'betterdocs-all-docs',
1194 'betterdocs_page_add_new' => 'betterdocs-add-new',
1195 'betterdocs-doc-categories' => 'betterdocs-categories',
1196 'betterdocs-doc-tags' => 'betterdocs-tags',
1197 'betterdocs-settings' => 'betterdocs-settings',
1198 'betterdocs-analytics' => 'betterdocs-analytics',
1199 'betterdocs-faq' => 'betterdocs-faq',
1200 'betterdocs-glossaries' => 'betterdocs-glossaries',
1201 'betterdocs-ai-chatbot' => 'betterdocs-ai-chatbot',
1202 'betterdocs-api-docs' => 'betterdocs-api-docs',
1203 'edit-tags.php?taxonomy=knowledge_base&post_type=docs' => 'betterdocs-multiplekb',
1204 );
1205
1206 foreach ( $menu as &$item ) {
1207 if ( isset( $menu_items[ $item[2] ] ) ) {
1208 if ( ! isset( $item[4] ) ) {
1209 $item[4] = '';
1210 }
1211 $item[4] .= '' . $menu_items[ $item[2] ];
1212 }
1213 }
1214
1215 foreach ( $submenu as &$submenu_items ) {
1216 foreach ( $submenu_items as &$sub_item ) {
1217 if ( isset( $menu_items[ $sub_item[2] ] ) ) {
1218 if ( ! isset( $sub_item[4] ) ) {
1219 $sub_item[4] = '';
1220 }
1221 $sub_item[4] .= '' . $menu_items[ $sub_item[2] ];
1222 }
1223 }
1224 }
1225 }
1226
1227 public function quick_setup_menu( $menus ) {
1228 $betterdocs_settings = get_option( 'betterdocs_settings' );
1229 if ( $betterdocs_settings ) {
1230 return $menus;
1231 } else {
1232 $menus['quick_setup'] = $this->normalize_menu(
1233 __( 'Quick Setup', 'betterdocs' ),
1234 'betterdocs-setup',
1235 'delete_users',
1236 array(
1237 $this->container->get( SetupWizard::class ),
1238 'views',
1239 )
1240 );
1241 }
1242
1243 return $menus;
1244 }
1245
1246 public function insert_plugin_links( $links ) {
1247 $links[] = '<a href="admin.php?page=betterdocs-settings">' . __( 'Settings', 'betterdocs' ) . '</a>';
1248
1249 if ( ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) {
1250 $links[] = '<a href="https://betterdocs.co/upgrade-to-pro-plugins-wp" target="_blank" style="color: #000; font-weight: bold;">' . __( 'Upgrade to Pro', 'betterdocs' ) . '</a>';
1251 }
1252
1253 return $links;
1254 }
1255
1256 public function toolbar_menu( $admin_bar ) {
1257 if ( ! is_admin() || ! is_admin_bar_showing() ) {
1258 return;
1259 }
1260
1261 // Show only when the user is a member of this site, or they're a super admin.
1262 if ( ! is_user_member_of_blog() && ! is_super_admin() ) {
1263 return;
1264 }
1265
1266 $docs_url = '';
1267 $encyclopedia_url = '';
1268
1269 if ( $this->settings->get( 'builtin_doc_page' ) ) {
1270 $docs_url = get_post_type_archive_link( 'docs' );
1271 } elseif ( intval( $docs_page = $this->settings->get( 'docs_page' ) ) ) {
1272 $docs_url = ! empty( $docs_page ) ? get_page_link( $docs_page ) : false;
1273 }
1274
1275 if ( ! $docs_url ) {
1276 return;
1277 }
1278
1279 $slug = $this->settings->get( 'encyclopedia_root_slug' );
1280
1281 global $wp_rewrite;
1282 if ( $wp_rewrite->using_index_permalinks() ) {
1283 $slug = $wp_rewrite->index . '/' . $slug;
1284 }
1285
1286 $encyclopedia_url = home_url( $slug );
1287
1288 $admin_bar->add_node(
1289 array(
1290 'parent' => 'site-name',
1291 'id' => 'view-docs',
1292 'title' => __( 'Visit Documentation', 'betterdocs' ),
1293 'href' => $docs_url,
1294 )
1295 );
1296
1297 $is_enable_encyclopedia = betterdocs()->settings->get( 'enable_encyclopedia' );
1298
1299 if ( $is_enable_encyclopedia && betterdocs()->is_pro_active() ) {
1300 $admin_bar->add_node(
1301 array(
1302 'parent' => 'site-name',
1303 'id' => 'view-encyclopedia',
1304 'title' => __( 'Visit Encyclopedia', 'betterdocs' ),
1305 'href' => $encyclopedia_url,
1306 )
1307 );
1308 }
1309 }
1310
1311 /**
1312 * Save last visited admin ui
1313 *
1314 * @since 3.0.1
1315 */
1316 public function save_admin_page() {
1317 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1318 $post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
1319 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1320 $bdocs_view = isset( $_GET['bdocs_view'] ) ? sanitize_text_field( wp_unslash( $_GET['bdocs_view'] ) ) : '';
1321 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1322 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
1323
1324 if ( 'docs' === $post_type && 'classic' === $bdocs_view ) {
1325 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'classic_ui' );
1326 } elseif ( 'betterdocs-admin' === $page ) {
1327 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'modern_ui' );
1328 }
1329 }
1330
1331 /**
1332 * Return last visited admin ui slug
1333 *
1334 * @return string
1335 * @since 3.0.1
1336 */
1337 public function ui_slug() {
1338 $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1339 $docs_exist = get_posts(
1340 array(
1341 'post_type' => 'docs',
1342 'post_status' => 'any',
1343 'numberposts' => 1,
1344 )
1345 );
1346
1347 return ( 'modern_ui' === $last_visited || empty( $docs_exist ) )
1348 ? 'betterdocs-admin'
1349 : 'edit.php?post_type=docs&bdocs_view=classic';
1350 }
1351
1352 /**
1353 * Resets a duplicate submenu in WordPress if the parent main menu and the first submenu permalink are not the same.
1354 *
1355 * @return string
1356 * @since 3.0.1
1357 */
1358 public function reset_submenu() {
1359 global $submenu;
1360
1361 $docs = get_posts( array( 'post_type' => 'docs' ) );
1362 if ( count( $docs ) == 0 ) {
1363 return;
1364 }
1365
1366 $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1367
1368 if ( 'classic_ui' === $last_visited && isset( $submenu['betterdocs-admin'] ) && in_array( 'betterdocs-admin', $submenu['betterdocs-admin'][0] ) ) {
1369 unset( $submenu['betterdocs-admin'][0] );
1370 $submenu['betterdocs-admin'] = array_values( $submenu['betterdocs-admin'] );
1371 }
1372 }
1373
1374 /**
1375 * Initialize Black Friday Pointer
1376 *
1377 * @return void
1378 * @since 3.7.0
1379 */
1380 private function init_black_friday_pointer() {
1381 // Only initialize if conditions are met
1382 if ( NoticePointers::should_display_notice() ) {
1383 new NoticePointers();
1384 }
1385 }
1386
1387 /**
1388 * AJAX handler for dismissing Black Friday pointer
1389 *
1390 * @return void
1391 * @since 3.7.0
1392 */
1393 public function ajax_dismiss_black_friday_pointer() {
1394 // Verify nonce
1395 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
1396 if ( ! wp_verify_nonce( $nonce, 'betterdocs_dismiss_pointer' ) ) {
1397 wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'betterdocs' ) ) );
1398 }
1399
1400 // Check if user has permission
1401 if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'edit_docs' ) ) {
1402 wp_send_json_error( array( 'message' => __( 'Permission denied', 'betterdocs' ) ) );
1403 }
1404
1405 // Get the introduction key
1406 $introduction_key = isset( $_POST['introduction_key'] ) ? sanitize_text_field( wp_unslash( $_POST['introduction_key'] ) ) : '';
1407
1408 if ( empty( $introduction_key ) ) {
1409 wp_send_json_error( array( 'message' => __( 'Invalid introduction key', 'betterdocs' ) ) );
1410 }
1411
1412 // Set the introduction as viewed
1413 NoticePointers::set_introduction_viewed( $introduction_key );
1414
1415 // Clear the priority option so other plugins can set their priority
1416 delete_option( '_wpdeveloper_plugin_pointer_priority' );
1417
1418 wp_send_json_success( array( 'message' => __( 'Pointer dismissed successfully', 'betterdocs' ) ) );
1419 }
1420
1421 /**
1422 * Get count of docs that are not yet synced
1423 * Similar to Helper::get_not_synced_post_ids() in betterdocs-ai-chatbot plugin
1424 *
1425 * @return int
1426 * @since 3.7.0
1427 */
1428 private function get_not_synced_docs_count() {
1429 $new_post_ids = get_option( 'saved_docs_post_ids', array() );
1430 $error_posts_data = get_option( 'betterdocs_ai_chatbot_error_posts', array() );
1431
1432 // Extract post IDs from error posts (handle both old and new formats)
1433 $error_post_ids = array();
1434 if ( is_array( $error_posts_data ) && ! empty( $error_posts_data ) ) {
1435 foreach ( $error_posts_data as $key => $value ) {
1436 if ( is_numeric( $key ) && is_numeric( $value ) ) {
1437 // Old format: numeric key with post ID as value
1438 $error_post_ids[] = $value;
1439 } elseif ( is_numeric( $key ) && is_array( $value ) && isset( $value['post_id'] ) ) {
1440 // New format: post ID as key with structured data
1441 $error_post_ids[] = $key;
1442 } elseif ( is_numeric( $key ) ) {
1443 // New format: post ID as key
1444 $error_post_ids[] = $key;
1445 }
1446 }
1447 }
1448
1449 // Merge both arrays and remove duplicates, then count
1450 return count( array_unique( array_merge( $new_post_ids, $error_post_ids ) ) );
1451 }
1452 }
1453