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

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