PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
← All changes | includes/Core/Admin.php +348 -879 4.9.04.2.0 View file →
@@ -1,20 +1,15 @@
1 1 <?php
2 +
2 3 namespace WPDeveloper\BetterDocs\Core;
3 4
4 -if ( ! defined( 'ABSPATH' ) ) {
5 - exit;
6 -}
7 -
8 -
9 5 use Exception;
10 6 use PriyoMukul\WPNotice\Notices;
11 -use WPDeveloper\BetterDocs\Admin\NoticePointers;
12 7 use WPDeveloper\BetterDocs\Utils\Base;
13 8 use PriyoMukul\WPNotice\Utils\CacheBank;
14 9 use WPDeveloper\BetterDocs\Utils\Helper;
15 10 use WPDeveloper\BetterDocs\Utils\Enqueue;
16 -use WPDeveloper\BetterDocs\Insights\Insights;
11 +use WPDeveloper\BetterDocs\Utils\Insights;
17 12 use PriyoMukul\WPNotice\Utils\NoticeRemover;
18 13 use WPDeveloper\BetterDocs\Core\PluginInstaller;
19 14 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
20 15
@@ -19,41 +14,13 @@
19 14 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
20 15
21 16 class Admin extends Base {
22 17 /**
23 - * Per-user flag recording that this administrator has opened the MCP screen.
24 - *
25 - * Stores the timestamp of the first visit, but only its **presence** is read:
26 - * absent means "this user has not seen MCP yet", which is what puts the
27 - * one-time discovery badge on the menu (ADR-063). Per user on purpose — two
28 - * administrators each get their own first look, and neither clears the other's.
29 - *
30 - * @var string
31 - * @since 4.9.0
32 - */
33 - const MCP_SEEN_META = 'betterdocs_mcp_seen';
34 -
35 - /**
36 - * Whether this request painted the MCP discovery badge onto the menu.
37 - *
38 - * Decided once in `menus()` (on `admin_menu`) and read again in
39 - * `mcp_badge_styles()` (on `admin_head`), rather than re-deciding, because the
40 - * two must agree: on the request that opens the MCP screen the badge is still
41 - * painted while the meta is already written, and re-deciding at `admin_head`
42 - * would leave that one painted pill unstyled.
43 - *
44 - * @var bool
45 - * @since 4.9.0
46 - */
47 - private $mcp_badge = false;
48 -
49 - /**
50 18 * @var CacheBank
51 19 */
52 20 private static $cache_bank;
53 21 /**
54 22 * Admin Root Menu Slug
55 - *
56 23 * @var string
57 24 */
58 25 private $slug = 'betterdocs-dashboard';
59 26 /**
@@ -85,19 +52,17 @@
85 52 private $kbmigration;
86 53
87 54 /**
88 55 * Enqueue
89 - *
90 56 * @var Enqueue
91 57 */
92 58 private $assets;
93 59
94 60 // modules
95 - protected $installer;
61 + protected $installer;
96 62
97 63 /**
98 64 * FAQBuilder
99 - *
100 65 * @var FAQBuilder
101 66 */
102 67 private $faq_builder;
103 68 private $glossaries;
@@ -108,10 +73,10 @@
108 73 $this->settings = $settings;
109 74 $this->kbmigration = $kbmigration;
110 75 $this->slug = 'betterdocs-dashboard';
111 76
112 - add_action( 'init', array( $type, 'register' ), 9 );
113 - add_action( 'rest_api_init', array( $this, 'order_terms_in_wp_terms_admin_table' ) );
77 + add_action( 'init', [ $type, 'register' ], 9 );
78 + add_action('rest_api_init', [$this, 'order_terms_in_wp_terms_admin_table']);
114 79
115 80 $type->init();
116 81 $type->admin_init();
117 82
@@ -117,18 +82,8 @@
117 82
118 83 $this->faq_builder = $this->container->get( FAQBuilder::class );
119 84 $this->glossaries = $this->container->get( Glossaries::class );
120 85
121 - /**
122 - * Register usage tracking (including the daily `put_do_weekly_action` cron
123 - * handler) on every request — WP-Cron runs with is_admin() === false, so
124 - * this MUST sit above the admin guard or the cron send never fires. The
125 - * admin-only UI hooks inside Insights::init() (deactivation form, footer
126 - * scripts, plugin_action_links) are context-specific and simply never run
127 - * outside wp-admin.
128 - */
129 - $this->plugin_insights();
130 -
131 86 if ( ! is_admin() ) {
132 87 return;
133 88 }
134 89
@@ -133,64 +88,47 @@
133 88 }
134 89
135 90 $this->installer = new PluginInstaller();
136 91
137 - add_action( 'admin_notices', array( $this, 'compatibility_notices' ) );
138 - // The WPNotice CacheBank wipes all admin_notices at priority 10 on BetterDocs
139 - // screens, so the hook above never renders inside the BetterDocs panels.
140 - // Re-add the compatibility notice after that wipe (in_admin_header, priority
141 - // 999) so it shows on the panels like the review / license notices.
142 - add_action( 'in_admin_header', function () {
143 - $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
144 - if ( $screen && betterdocs()->is_betterdocs_screen( $screen->id ) ) {
145 - add_action( 'admin_notices', array( $this, 'compatibility_notices' ) );
146 - }
147 - }, 999 );
92 + $this->plugin_insights();
93 + add_action( 'admin_notices', [ $this, 'compatibility_notices' ] );
148 94 // add_action( 'admin_init', [$this, 'notices'], 9 );
149 - add_filter( 'admin_init', array( $this, 'save_admin_page' ), 99 );
95 + add_filter( 'admin_init', [ $this, 'save_admin_page' ], 99 );
150 96
151 - add_action( 'admin_menu', array( $this, 'menus' ) );
152 - // The badge's clear runs on `admin_init` — a hook that fires for every
153 - // admin request — and identifies the screen by its page slug, rather
154 - // than on `load-{$hook_suffix}` (ADR-065). `admin_init` fires *after*
155 - // `admin_menu`, measured on the rig, so the badge is still painted on
156 - // the request that opens the screen exactly as before.
157 - add_action( 'admin_init', array( $this, 'mark_mcp_seen' ) );
158 - add_action( 'admin_menu', array( $this, 'reset_submenu' ) );
159 - add_action( 'admin_head', array( $this, 'add_custom_classes_to_menu_items' ) );
160 - add_action( 'admin_head', array( $this, 'mcp_badge_styles' ) );
161 - add_filter( 'plugin_action_links_' . BETTERDOCS_PLUGIN_BASENAME, array( $this, 'insert_plugin_links' ) );
97 + add_action( 'admin_menu', [ $this, 'menus' ] );
98 + add_action( 'admin_menu', [ $this, 'reset_submenu' ] );
99 + add_action( 'admin_head', [ $this, 'add_custom_classes_to_menu_items' ] );
100 + add_filter( 'plugin_action_links_' . BETTERDOCS_PLUGIN_BASENAME, [ $this, 'insert_plugin_links' ] );
162 101
163 102 // $this->container->get( SetupWizard::class )->init();
164 103
165 - add_action( 'admin_enqueue_scripts', array( $this, 'styles' ) );
166 - add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
167 - // add_action( 'betterdocs_listing_header', [ $this, 'header' ], 10, 1 );
168 - add_action( 'admin_bar_menu', array( $this, 'toolbar_menu' ), 32 );
104 + add_action( 'admin_enqueue_scripts', [ $this, 'styles' ] );
105 + add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
106 + //add_action( 'betterdocs_listing_header', [ $this, 'header' ], 10, 1 );
107 + add_action( 'admin_bar_menu', [ $this, 'toolbar_menu' ], 32 );
169 108
170 - add_filter( 'admin_body_class', array( $this, 'body_classes' ) );
171 - add_filter( 'parent_file', array( $type, 'highlight_admin_menu' ) );
172 - add_filter( 'submenu_file', array( $type, 'highlight_admin_submenu' ), 10, 2 );
173 - add_filter( 'betterdocs_admin_menu', array( $this, 'quick_setup_menu' ), 10, 1 );
109 + add_filter( 'admin_body_class', [ $this, 'body_classes' ] );
110 + add_filter( 'parent_file', [ $type, 'highlight_admin_menu' ] );
111 + add_filter( 'submenu_file', [ $type, 'highlight_admin_submenu' ], 10, 2 );
112 + add_filter( 'betterdocs_admin_menu', [ $this, 'quick_setup_menu' ], 10, 1 );
174 113
175 114 /**
176 115 * Remove Comments Column from List Table.
177 116 */
178 - add_filter( 'manage_docs_posts_columns', array( $this, 'set_custom_edit_action_columns' ) );
179 - add_filter( 'manage_docs_posts_custom_column', array( $this, 'manage_custom_columns' ), 10, 2 );
117 + add_filter( 'manage_docs_posts_columns', [ $this, 'set_custom_edit_action_columns' ] );
118 + add_filter( 'manage_docs_posts_custom_column', [ $this, 'manage_custom_columns' ], 10, 2 );
180 119
181 120 /**
182 121 * Add New Column
183 122 */
184 - add_filter( 'manage_users_columns', array( $this, 'add_users_total_docs_column' ), 10, 1 );
185 - add_filter( 'manage_users_custom_column', array( $this, 'popular_users_docs_data' ), 10, 3 );
123 + add_filter( 'manage_users_columns', [ $this, 'add_users_total_docs_column' ], 10, 1 );
124 + add_filter( 'manage_users_custom_column', [ $this, 'popular_users_docs_data' ], 10, 3 );
186 125 if ( is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) {
187 126 add_action( 'admin_footer-plugins.php', array( $this, 'disable_deactivation' ) );
188 127 }
189 128
190 129 if ( $this->settings->get( 'enable_estimated_reading_time' ) ) {
191 - // Hook into unified metabox instead of creating separate metabox
192 - add_action( 'betterdocs_reading_time_tab_content', array( $this, 'render_estimated_time_markup' ) );
130 + add_action( 'add_meta_boxes', [ $this, 'reading_meta_box_' ], 10 );
193 131 }
194 132
195 133 self::$cache_bank = CacheBank::get_instance();
196 134
@@ -201,28 +139,17 @@
201 139 $this->notices();
202 140 } catch ( Exception $e ) {
203 141 unset( $e );
204 142 }
205 -
206 - // Initialize Black Friday Pointer
207 - $this->init_black_friday_pointer();
208 -
209 - // Register AJAX handler for pointer dismissal
210 - add_action( 'wp_ajax_betterdocs_dismiss_black_friday_pointer', array( $this, 'ajax_dismiss_black_friday_pointer' ) );
211 143 }
212 144
213 145 public function order_terms_in_wp_terms_admin_table() {
214 - // order the terms correctly to be shown on the admin panel categories menu with betterdocs order
215 - add_action(
216 - 'rest_insert_doc_category',
217 - function ( $term, $request, $bool ) {
218 - $max_order = Helper::get_max_doc_category_order_from_term_meta() ?? 0;
219 - $next_order = $max_order + 1;
220 - update_term_meta( $term->term_id, 'doc_category_order', $next_order );
221 - },
222 - 10,
223 - 3
224 - );
146 + //order the terms correctly to be shown on the admin panel categories menu with betterdocs order
147 + add_action('rest_insert_doc_category', function( $term, $request, $bool ) {
148 + $max_order = Helper::get_max_doc_category_order_from_term_meta() ?? 0;
149 + $next_order = $max_order + 1;
150 + update_term_meta( $term->term_id, 'doc_category_order', $next_order );
151 + }, 10, 3);
225 152 }
226 153
227 154 public function disable_deactivation() {
228 155 $tooltip_text = esc_html__( 'Deactivate BetterDocs Pro First', 'betterdocs' );
@@ -269,17 +196,17 @@
269 196 <?php
270 197 }
271 198
272 199 public function add_users_total_docs_column( $columns ) {
273 - $new_column = array(
274 - 'docs' => __( 'Docs', 'betterdocs' ),
275 - );
200 + $new_column = [
201 + 'docs' => __( 'Docs', 'betterdocs' )
202 + ];
276 203 $columns = array_merge( $columns, $new_column );
277 204 return $columns;
278 205 }
279 206
280 207 public function popular_users_docs_data( $output, $column_name, $user_id ) {
281 - if ( 'docs' == $column_name ) {
208 + if ( $column_name == 'docs' ) {
282 209 $total_count = count_user_posts( $user_id, 'docs', true );
283 210 return '<a href="edit.php?post_type=docs&author=' . $user_id . '" class="edit"><span aria-hidden="true">' . $total_count . '</span></a>';
284 211 }
285 212 return $output;
@@ -284,8 +211,20 @@
284 211 }
285 212 return $output;
286 213 }
287 214
215 + public function reading_meta_box_() {
216 + add_meta_box(
217 + 'betterdocs_estimated_time_metabox',
218 + __( 'Estimated Reading Time', 'betterdocs' ),
219 + [
220 + $this,
221 + 'render_estimated_time_markup'
222 + ],
223 + 'docs'
224 + );
225 + }
226 +
288 227 public function render_estimated_time_markup() {
289 228 betterdocs()->views->get( 'admin/metabox/estimated-reading-box' );
290 229 }
291 230
@@ -293,15 +232,13 @@
293 232 if ( betterdocs()->is_pro_active() ) {
294 233 $plugins = Helper::get_plugins();
295 234 $plugin_data = $plugins['betterdocs-pro/betterdocs-pro.php'];
296 235
297 - // Require the paired Pro release: the Analytics UI is version-coupled to
298 - // Pro's advanced modules, so an older Pro renders a broken/partial panel.
299 - if ( isset( $plugin_data['Version'] ) && version_compare( $plugin_data['Version'], '4.0.0', '>=' ) ) {
236 + if ( isset( $plugin_data['Version'] ) && version_compare( $plugin_data['Version'], '2.5.0', '>=' ) ) {
300 237 return;
301 238 }
302 239
303 - betterdocs()->views->get( 'admin/notices/compatibility', array( 'version' => $plugin_data['Version'] ) );
240 + betterdocs()->views->get( 'admin/notices/compatibility', [ 'version' => $plugin_data['Version'] ] );
304 241 }
305 242 }
306 243
307 244 public function plugin_insights( $prevent_init = false ) {
@@ -306,20 +243,20 @@
306 243
307 244 public function plugin_insights( $prevent_init = false ) {
308 245 $this->insights = Insights::get_instance(
309 246 BETTERDOCS_PLUGIN_FILE,
310 - array(
247 + [
311 248 'opt_in' => true,
312 249 'goodbye_form' => true,
313 - 'item_id' => 'c7b16777b4f1b83f6083',
314 - )
250 + 'item_id' => 'c7b16777b4f1b83f6083'
251 + ]
315 252 );
316 253
317 254 $this->insights->set_notice_options(
318 - array(
255 + [
319 256 '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' ),
320 - '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' ),
321 - )
257 + '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' )
258 + ]
322 259 );
323 260
324 261 if ( ! $prevent_init ) {
325 262 $this->insights->init();
@@ -335,77 +272,76 @@
335 272 * @throws Exception
336 273 */
337 274 public function notices() {
338 275 $notices = new Notices(
339 - array(
276 + [
340 277 'id' => 'betterdocs',
341 278 'storage_key' => 'notices',
342 279 'lifetime' => 3,
343 280 'stylesheet_url' => $this->assets->asset_url( 'admin/css/notices.css' ),
344 281 'styles' => $this->assets->asset_url( 'admin/css/notices.css' ),
345 - 'priority' => 4,
346 - )
282 + 'priority' => 4
283 + ]
347 284 );
348 285
349 286 /**
350 - * Review Notice
351 - *
352 - * @var mixed $message
353 - */
287 + * Review Notice
288 + * @var mixed $message
289 + */
354 290
355 291 $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' );
356 292
357 - $_review_notice = array(
293 + $_review_notice = [
358 294 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ),
359 295 'html' => '<p>' . $message . '</p>',
360 - 'links' => array(
361 - 'later' => array(
296 + 'links' => [
297 + 'later' => [
362 298 'link' => 'https://wordpress.org/plugins/betterdocs/#reviews',
363 299 'target' => '_blank',
364 300 'label' => __( 'Sure, you deserve it!', 'betterdocs' ),
365 - 'icon_class' => 'dashicons dashicons-external',
366 - ),
367 - 'allready' => array(
301 + 'icon_class' => 'dashicons dashicons-external'
302 + ],
303 + 'allready' => [
368 304 'label' => __( 'I already did', 'betterdocs' ),
369 305 'icon_class' => 'dashicons dashicons-smiley',
370 - 'attributes' => array(
371 - 'data-dismiss' => true,
372 - ),
373 - ),
374 - 'maybe_later' => array(
306 + 'attributes' => [
307 + 'data-dismiss' => true
308 + ]
309 + ],
310 + 'maybe_later' => [
375 311 'label' => __( 'Maybe Later', 'betterdocs' ),
376 312 'icon_class' => 'dashicons dashicons-calendar-alt',
377 - 'attributes' => array(
313 + 'attributes' => [
378 314 'data-later' => true,
379 - 'class' => 'dismiss-btn',
380 - ),
381 - ),
382 - 'support' => array(
315 + 'class' => 'dismiss-btn'
316 + ]
317 + ],
318 + 'support' => [
383 319 'link' => 'https://wpdeveloper.com/support',
384 - 'attributes' => array(
385 - 'target' => '_blank',
386 - ),
320 + 'attributes' => [
321 + 'target' => '_blank'
322 + ],
387 323 'label' => __( 'I need help', 'betterdocs' ),
388 - 'icon_class' => 'dashicons dashicons-sos',
389 - ),
390 - 'never_show_again' => array(
324 + 'icon_class' => 'dashicons dashicons-sos'
325 + ],
326 + 'never_show_again' => [
391 327 'label' => __( 'Never show again', 'betterdocs' ),
392 328 'icon_class' => 'dashicons dashicons-dismiss',
393 - 'attributes' => array(
394 - 'data-dismiss' => true,
395 - ),
396 - ),
397 - ),
398 - );
329 + 'attributes' => [
330 + 'data-dismiss' => true
331 + ]
332 + ]
333 + ]
334 + ];
399 335
400 336 $notices->add(
401 337 'review',
402 338 $_review_notice,
403 - array(
339 + [
404 340 'start' => $notices->strtotime( '+10 days' ),
405 341 'recurrence' => 30,
406 - 'dismissible' => true,
407 - )
342 + 'dismissible' => true
343 + ]
408 344 );
409 345
410 346 if ( $this->kbmigration->existing_plugins && ! in_array( $this->kbmigration->existing_plugins[0][0], $this->kbmigration->migrated_plugins ) ) {
411 347 $plugin_name = '<strong>' . esc_html( $this->kbmigration->existing_plugins[0][1] ) . '</strong>';
@@ -410,9 +346,9 @@
410 346 if ( $this->kbmigration->existing_plugins && ! in_array( $this->kbmigration->existing_plugins[0][0], $this->kbmigration->migrated_plugins ) ) {
411 347 $plugin_name = '<strong>' . esc_html( $this->kbmigration->existing_plugins[0][1] ) . '</strong>';
412 348
413 349 $message = sprintf(
414 - /* translators: %s is the name of the existing knowledge base plugin. */
350 + /* translators: %s is the name of the existing knowledge base plugin. */
415 351 __( 'Already using %s? Power up your Knowledge Base by migrating all your docs and settings to BetterDocs with just 1 click.', 'betterdocs' ),
416 352 esc_html( $plugin_name )
417 353 );
418 354
@@ -422,51 +358,50 @@
422 358 esc_url( admin_url( 'admin.php?page=betterdocs-settings&tab=tab-migration' ) ),
423 359 esc_html__( 'Start Migration', 'betterdocs' )
424 360 );
425 361
426 - $_migration_notice = array(
362 + $_migration_notice = [
427 363 'thumbnail' => '',
428 364 'html' => $migration_message,
429 - 'links' => array(
430 - 'maybe_later' => array(
365 + 'links' => [
366 + 'maybe_later' => [
431 367 'label' => __( 'Maybe Later', 'betterdocs' ),
432 368 'icon_class' => 'dashicons dashicons-calendar-alt',
433 - 'attributes' => array(
369 + 'attributes' => [
434 370 'data-later' => true,
435 - 'class' => 'dismiss-btn',
436 - ),
437 - ),
438 - 'never_show_again' => array(
371 + 'class' => 'dismiss-btn'
372 + ]
373 + ],
374 + 'never_show_again' => [
439 375 'label' => __( 'Never show again', 'betterdocs' ),
440 376 'icon_class' => 'dashicons dashicons-dismiss',
441 - 'attributes' => array(
442 - 'data-dismiss' => true,
443 - ),
444 - ),
445 - ),
446 - );
377 + 'attributes' => [
378 + 'data-dismiss' => true
379 + ]
380 + ]
381 + ]
382 + ];
447 383
448 384 $notices->add(
449 385 'migration',
450 386 $_migration_notice,
451 - array(
387 + [
452 388 'start' => $notices->time(),
453 389 'recurrence' => false,
454 - 'dismissible' => true,
455 - )
390 + 'dismissible' => true
391 + ]
456 392 );
457 393 }
458 394
459 395 /**
460 - *
461 396 * Opt-In Notice
462 397 */
463 398 $allow_tracking = get_option( 'wpins_allow_tracking' );
464 - if ( null != $this->insights && ! isset( $allow_tracking['betterdocs'] ) ) {
399 + if ( $this->insights != null && ! isset( $allow_tracking['betterdocs'] ) ) {
465 400 $notices->add(
466 401 'opt_in',
467 - array( $this->insights, 'notice' ),
468 - array(
402 + [ $this->insights, 'notice' ],
403 + [
469 404 'classes' => 'updated put-dismiss-notice',
470 405 'start' => $notices->time(),
471 406 'refresh' => BETTERDOCS_VERSION,
472 407 'dismissible' => true,
@@ -471,48 +406,55 @@
471 406 'refresh' => BETTERDOCS_VERSION,
472 407 'dismissible' => true,
473 408 'do_action' => 'wpdeveloper_notice_clicked_for_betterdocs',
474 409 'display_if' => ! function_exists( 'betterdocs_pro' ),
475 - 'screens' => array( 'dashboard' ),
476 - )
410 + 'screens' => [ 'dashboard' ]
411 + ]
477 412 );
478 413 }
479 414
480 - $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>';
481 - $_summer_campaign_notice = array(
415 + // $blackfriday_message = '<div class="betterdocs-notice-body"><p style="margin-top: 0; margin-bottom: 0;"><strong>🛍️ Black Friday Specials:</strong> Save up to 35% OFF on BetterDocs PRO plans & manage docs/FAQs seamlessly.</p><a class="button button-primary" href="https://betterdocs.co/bfcm24-pricing" target="_blank"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
416 + // <path d="M15.7431 10.9381L15.904 9.35966C15.9898 8.5175 16.0464 7.9614 16.002 7.61102L16.0175 7.61112C16.7442 7.61112 17.3333 6.98929 17.3333 6.22223C17.3333 5.45517 16.7442 4.83334 16.0175 4.83334C15.2908 4.83334 14.7017 5.45517 14.7017 6.22223C14.7017 6.56914 14.8222 6.88634 15.0214 7.12975C14.7354 7.31608 14.3615 7.70926 13.7987 8.30106L13.7986 8.30107L13.7986 8.30108C13.365 8.75699 13.1482 8.98495 12.9064 9.02025C12.7724 9.03981 12.6358 9.01971 12.5121 8.96219C12.2887 8.85838 12.1398 8.57656 11.842 8.01293L10.2723 5.04204C10.0886 4.69433 9.9348 4.40331 9.79616 4.16913C10.3649 3.86285 10.7543 3.23869 10.7543 2.51852C10.7543 1.49577 9.96888 0.666672 8.99996 0.666672C8.03104 0.666672 7.24557 1.49577 7.24557 2.51852C7.24557 3.23869 7.63503 3.86285 8.20376 4.16913C8.06511 4.40333 7.91137 4.6943 7.72763 5.04204L6.1579 8.01293C5.8601 8.57656 5.71119 8.85838 5.48786 8.96219C5.36411 9.01971 5.22757 9.03981 5.09355 9.02025C4.85169 8.98495 4.63488 8.75699 4.20127 8.30107C3.63844 7.70928 3.26449 7.31608 2.97849 7.12975C3.17771 6.88634 3.29821 6.56914 3.29821 6.22223C3.29821 5.45517 2.70911 4.83334 1.98242 4.83334C1.25572 4.83334 0.666626 5.45517 0.666626 6.22223C0.666626 6.98929 1.25572 7.61112 1.98242 7.61112L1.99795 7.61102C1.95348 7.96139 2.01015 8.51749 2.09596 9.35965L2.2568 10.938C2.34608 11.8142 2.42032 12.6478 2.51125 13.3982H15.4887C15.5796 12.6478 15.6538 11.8142 15.7431 10.9381Z" fill="white"/>
417 + // <path d="M8.04563 17.3333H9.95429C12.4419 17.3333 13.6858 17.3333 14.5157 16.5492C14.8779 16.207 15.1073 15.59 15.2728 14.787H2.72711C2.89263 15.59 3.12201 16.207 3.48424 16.5492C4.31414 17.3333 5.55797 17.3333 8.04563 17.3333Z" fill="white"/>
418 + // </svg> Upgrade To PRO</a></div>';
419 + // $_blackfriday_notice = [
420 + // 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ),
421 + // 'html' => $blackfriday_message
422 + // ];
423 +
424 + // $notices->add(
425 + // 'blackfriday24',
426 + // $_blackfriday_notice,
427 + // [
428 + // 'start' => $notices->time(),
429 + // 'recurrence' => false,
430 + // 'dismissible' => true,
431 + // 'refresh' => BETTERDOCS_VERSION,
432 + // 'expire' => strtotime( '11:59:59pm December 5, 2024' ),
433 + // 'display_if' => ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' )
434 + // ]
435 + // );
436 +
437 + $offer_message = '<div class="betterdocs-notice-body betterdocs-holiday-notice-body"><p style="margin-top: 0; margin-bottom: 0;"><strong>🎉 BetterDocs Just Hit 40,000+ Users!</strong> Join the celebration with 20% OFF & unlock even more power with premium features today!</p><div class="betterdocs-notice-actions"><a class="button button-primary" href="https://betterdocs.co/40K-admin-notice" target="_blank"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
438 + <path d="M15.7431 10.9381L15.904 9.35966C15.9898 8.5175 16.0464 7.9614 16.002 7.61102L16.0175 7.61112C16.7442 7.61112 17.3333 6.98929 17.3333 6.22223C17.3333 5.45517 16.7442 4.83334 16.0175 4.83334C15.2908 4.83334 14.7017 5.45517 14.7017 6.22223C14.7017 6.56914 14.8222 6.88634 15.0214 7.12975C14.7354 7.31608 14.3615 7.70926 13.7987 8.30106L13.7986 8.30107L13.7986 8.30108C13.365 8.75699 13.1482 8.98495 12.9064 9.02025C12.7724 9.03981 12.6358 9.01971 12.5121 8.96219C12.2887 8.85838 12.1398 8.57656 11.842 8.01293L10.2723 5.04204C10.0886 4.69433 9.9348 4.40331 9.79616 4.16913C10.3649 3.86285 10.7543 3.23869 10.7543 2.51852C10.7543 1.49577 9.96888 0.666672 8.99996 0.666672C8.03104 0.666672 7.24557 1.49577 7.24557 2.51852C7.24557 3.23869 7.63503 3.86285 8.20376 4.16913C8.06511 4.40333 7.91137 4.6943 7.72763 5.04204L6.1579 8.01293C5.8601 8.57656 5.71119 8.85838 5.48786 8.96219C5.36411 9.01971 5.22757 9.03981 5.09355 9.02025C4.85169 8.98495 4.63488 8.75699 4.20127 8.30107C3.63844 7.70928 3.26449 7.31608 2.97849 7.12975C3.17771 6.88634 3.29821 6.56914 3.29821 6.22223C3.29821 5.45517 2.70911 4.83334 1.98242 4.83334C1.25572 4.83334 0.666626 5.45517 0.666626 6.22223C0.666626 6.98929 1.25572 7.61112 1.98242 7.61112L1.99795 7.61102C1.95348 7.96139 2.01015 8.51749 2.09596 9.35965L2.2568 10.938C2.34608 11.8142 2.42032 12.6478 2.51125 13.3982H15.4887C15.5796 12.6478 15.6538 11.8142 15.7431 10.9381Z" fill="white"/>
439 + <path d="M8.04563 17.3333H9.95429C12.4419 17.3333 13.6858 17.3333 14.5157 16.5492C14.8779 16.207 15.1073 15.59 15.2728 14.787H2.72711C2.89263 15.59 3.12201 16.207 3.48424 16.5492C4.31414 17.3333 5.55797 17.3333 8.04563 17.3333Z" fill="white"/>
440 + </svg>Upgrade To PRO</a></div></div>';
441 + $_offer_notice = [
482 442 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ),
483 - 'html' => $summer_campaign_message,
484 - 'links' => array(
485 - 'support' => array(
486 - 'link' => 'https://betterdocs.co/summer2026-admin-notice',
487 - 'attributes' => array(
488 - 'target' => '_blank',
489 - 'class' => 'offer-button',
490 - ),
491 - 'label' => __( 'Upgrade To PRO Now', 'betterdocs' ),
492 - ),
493 - 'maybe_later' => array(
494 - 'label' => __( 'I’ll Grab It Later', 'betterdocs' ),
495 - 'attributes' => array(
496 - 'target' => '_blank',
497 - 'data-later' => true,
498 - 'class' => 'dismiss-btn',
499 - ),
500 - ),
501 - ),
502 - );
443 + 'html' => $offer_message
444 + ];
503 445
504 446 $notices->add(
505 - 'summer-campaign-26',
506 - $_summer_campaign_notice,
507 - array(
447 + '40k_offer',
448 + $_offer_notice,
449 + [
508 450 'start' => $notices->time(),
509 451 'recurrence' => false,
510 452 'dismissible' => true,
511 453 'refresh' => BETTERDOCS_VERSION,
512 - 'expire' => strtotime( '11:59:59pm June 25, 2026' ),
513 - 'display_if' => ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ),
514 - )
454 + 'expire' => strtotime( '11:59:59pm April 30, 2025' ),
455 + 'display_if' => ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' )
456 + ]
515 457 );
516 458
517 459 self::$cache_bank->create_account( $notices );
518 460 self::$cache_bank->calculate_deposits( $notices );
@@ -517,9 +459,9 @@
517 459 self::$cache_bank->create_account( $notices );
518 460 self::$cache_bank->calculate_deposits( $notices );
519 461 if ( method_exists( self::$cache_bank, 'clear_notices_in_' ) ) {
520 462 self::$cache_bank->clear_notices_in_(
521 - array(
463 + [
522 464 'toplevel_page_betterdocs-dashboard',
523 465 'admin_page_betterdocs-admin',
524 466 'betterdocs_page_betterdocs-admin',
525 467 'betterdocs_page_betterdocs-settings',
@@ -526,14 +468,11 @@
526 468 'betterdocs_page_betterdocs-faq',
527 469 'betterdocs_page_betterdocs-analytics',
528 470 'betterdocs_page_betterdocs-glossaries',
529 471 'betterdocs_page_betterdocs-ai-chatbot',
530 - 'betterdocs_page_betterdocs-api-docs',
531 - 'betterdocs_page_betterdocs-doc-categories',
532 - 'betterdocs_page_betterdocs-doc-tags',
533 472 'edit-doc_category',
534 - 'edit-doc_tag',
535 - ),
473 + 'edit-doc_tag'
474 + ],
536 475 $notices,
537 476 true
538 477 );
539 478 }
@@ -538,92 +477,31 @@
538 477 );
539 478 }
540 479 }
541 480
542 - /**
543 - * Resolve the admin dark-mode preference.
544 - *
545 - * The mode switcher stores the choice in a client cookie (no DB write, shared
546 - * across every admin screen). Fall back to the legacy
547 - * `betterdocs_settings['dark_mode']` value for installs that set it before this
548 - * change and haven't toggled since.
549 - *
550 - * @return bool
551 - */
552 - public function is_dark_mode() {
553 - if ( isset( $_COOKIE['betterdocs_admin_dark_mode'] ) ) {
554 - return '1' === $_COOKIE['betterdocs_admin_dark_mode']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
555 - }
556 -
557 - $saved = get_option( 'betterdocs_settings', array() );
558 - return ! empty( $saved['dark_mode'] );
559 - }
560 -
561 - /**
562 - * Whether the knowledge base is genuinely empty (no doc categories and no
563 - * non-trash docs). Localized to the admin so the All Docs panel can render a
564 - * skeleton shaped like the "No Category Found" empty card on first paint —
565 - * instead of a category/docs skeleton it would immediately replace — without
566 - * waiting for the REST fetch to reveal the count.
567 - *
568 - * @return bool
569 - */
570 - public function kb_is_empty() {
571 - $cats = wp_count_terms( array( 'taxonomy' => 'doc_category', 'hide_empty' => false ) );
572 - $cats = is_wp_error( $cats ) ? 0 : (int) $cats;
573 - if ( $cats > 0 ) {
574 - return false;
575 - }
576 -
577 - $counts = (array) wp_count_posts( 'docs' );
578 - $total = 0;
579 - foreach ( array( 'publish', 'future', 'draft', 'pending', 'private' ) as $status ) {
580 - $total += isset( $counts[ $status ] ) ? (int) $counts[ $status ] : 0;
581 - }
582 -
583 - return 0 === $total;
584 - }
585 -
586 481 public function body_classes( $classes ) {
587 - $dark_mode = $this->is_dark_mode();
482 + $saved_settings = get_option( 'betterdocs_settings', false );
483 + $dark_mode = isset( $saved_settings['dark_mode'] ) ? $saved_settings['dark_mode'] : false;
484 + $dark_mode = ! empty( $dark_mode ) ? boolval( $dark_mode ) : false;
588 485 $current_screen_id = get_current_screen() != null ? str_replace( 'betterdocs_page_', '', str_replace( 'toplevel_page_', '', str_replace( 'admin_page_', '', get_current_screen()->id ) ) ) : '';
589 - /**
590 - * Filter the list of (prefix-stripped) screen ids that receive the
591 - * `betterdocs-admin` body class (and dark-mode class). Pro/add-ons can
592 - * register their own React admin pages, e.g. the Knowledge Base page.
593 - *
594 - * @param string[] $registered_screens Screen ids with the page prefix removed.
595 - */
596 - $registered_screens = apply_filters( 'betterdocs_admin_screen_slugs', array(
597 - 'betterdocs-settings',
598 - 'betterdocs-admin',
599 - 'betterdocs-dashboard',
600 - 'betterdocs-analytics',
601 - 'betterdocs-glossaries',
602 - 'betterdocs-faq',
603 - 'betterdocs-doc-categories',
604 - 'betterdocs-doc-tags',
605 - 'edit-doc_category',
606 - 'edit-doc_tag',
607 - 'edit-knowledge_base',
608 - 'betterdocs-ai-chatbot',
609 - 'betterdocs-api-docs',
610 - // Without this the MCP screen never receives `betterdocs-admin`, and
611 - // the design tokens' dark-mode overrides — which are declared on
612 - // `.betterdocs-admin.betterdocs-dark-mode` — can never apply there:
613 - // the switcher in the header flips the cookie and the page stays
614 - // light. @since 4.9.0
615 - 'betterdocs-mcp',
616 - ) );
486 + $registered_screens = [
487 + 'betterdocs-settings',
488 + 'betterdocs-admin',
489 + 'betterdocs-dashboard',
490 + 'betterdocs-analytics',
491 + 'betterdocs-glossaries',
492 + 'betterdocs-faq',
493 + 'edit-doc_category',
494 + 'edit-doc_tag',
495 + 'edit-knowledge_base',
496 + 'betterdocs-ai-chatbot'
497 + ];
617 498
618 - if ( in_array( $current_screen_id, $registered_screens ) ) {
619 - $classes .= ' betterdocs-admin ';
620 - }
499 + if( in_array( $current_screen_id, $registered_screens ) ) {
500 + $classes .= ' betterdocs-admin ';
501 + }
621 502
622 - // Dark mode also applies on the Quick Setup wizard, whose self-scoped chrome
623 - // keys off `.betterdocs_page_betterdocs-setup.betterdocs-dark-mode`.
624 - $dark_screens = array_merge( $registered_screens, array( 'betterdocs-setup' ) );
625 - if ( $dark_mode && in_array( $current_screen_id, $dark_screens, true ) ) {
503 + if ( $dark_mode === true && in_array( $current_screen_id, $registered_screens ) ) {
626 504 $classes .= ' betterdocs-dark-mode ';
627 505 }
628 506
629 507 return $classes;
@@ -638,11 +516,11 @@
638 516 * @since 1.0.0
639 517 */
640 518 public function set_custom_edit_action_columns( $columns ) {
641 519 unset( $columns['comments'] );
642 - $new_columns = array();
520 + $new_columns = [];
643 521 foreach ( $columns as $key => $value ) {
644 - if ( 'date' == $key ) {
522 + if ( $key == 'date' ) {
645 523 $new_columns['betterdocs_word_count'] = __( 'Word Count', 'betterdocs' ); // put the tags column before it
646 524 $new_columns['betterdocs_reaction'] = __( 'Reactions', 'betterdocs' );
647 525 }
648 526 $new_columns[ $key ] = $value;
@@ -654,29 +532,26 @@
654 532 public function manage_custom_columns( $column, $post_id ) {
655 533 global $wpdb;
656 534 switch ( $column ) {
657 535 case 'betterdocs_word_count':
658 - $content_without_html_tags = trim( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );
536 + $content_without_html_tags = trim( strip_tags( get_post_field( 'post_content', $post_id ) ) );
659 537 preg_match_all( '/<[^>]*>|[\p{L}\p{M}]+/u', $content_without_html_tags, $matches );
660 - $total_words = ! empty( $matches[0] ) ? count( $matches[0] ) : count( array() );
538 + $total_words = ! empty( $matches[0] ) ? count( $matches[0] ) : count( [] );
661 539 $word_count = $total_words;
662 540 echo '<span>' . esc_html( intval( $word_count ) ) . '</span>';
663 541 break;
664 542 case 'betterdocs_reaction':
665 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- per-post analytics aggregation rendered in admin list table; cache would mask live reactions.
543 + $where = "WHERE post_id='" . esc_sql( $post_id ) . "'";
666 544 $analytics = $wpdb->get_results(
667 - $wpdb->prepare(
668 - "SELECT
669 - sum(impressions) as totalViews,
670 - sum(unique_visit) as totalUniqueViews,
671 - sum(happy + sad + normal) as totalReactions,
672 - sum(happy) as totalHappy,
673 - sum(normal) as totalNormal,
674 - sum(sad) as totalSad
675 - FROM {$wpdb->prefix}betterdocs_analytics
676 - WHERE post_id = %d",
677 - $post_id
678 - )
545 + "SELECT
546 + sum(impressions) as totalViews,
547 + sum(unique_visit) as totalUniqueViews,
548 + sum(happy + sad + normal) as totalReactions,
549 + sum(happy) as totalHappy,
550 + sum(normal) as totalNormal,
551 + sum(sad) as totalSad
552 + FROM {$wpdb->prefix}betterdocs_analytics
553 + $where"
679 554 );
680 555
681 556 echo '<ul class="reactions-count">
682 557 <li>
@@ -732,22 +607,22 @@
732 607 * @return void
733 608 * @since 1.0.0
734 609 */
735 610 public function styles( $hook ) {
736 - $this->assets->enqueue( 'betterdocs-global', 'admin/css/global.css', array(), 'all' );
611 + $this->assets->enqueue( 'betterdocs-global', 'admin/css/global.css', [], 'all' );
737 612
738 613 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
739 614 return;
740 615 }
741 616
742 - $this->assets->enqueue( 'betterdocs-select2', 'vendor/css/select2.min.css', array(), 'all' );
743 - $this->assets->enqueue( 'betterdocs-daterangepicker', 'vendor/css/daterangepicker.css', array(), 'all' );
744 - $this->assets->enqueue( 'betterdocs-old', 'admin/css/betterdocs.css', array(), 'all' );
617 + $this->assets->enqueue( 'betterdocs-select2', 'vendor/css/select2.min.css', [], 'all' );
618 + $this->assets->enqueue( 'betterdocs-daterangepicker', 'vendor/css/daterangepicker.css', [], 'all' );
619 + $this->assets->enqueue( 'betterdocs-old', 'admin/css/betterdocs.css', [], 'all' );
745 620
746 621 /**
747 - * This scripts enqueued for Dashboard App.
748 - */
749 - $this->assets->enqueue( 'betterdocs', 'admin/css/dashboard.css', array( 'betterdocs-old' ), '', BETTERDOCS_VERSION );
622 + * This scripts enqueued for Dashboard App.
623 + */
624 + $this->assets->enqueue( 'betterdocs', 'admin/css/dashboard.css', [ 'betterdocs-old' ], '', BETTERDOCS_VERSION );
750 625 $this->assets->enqueue( 'betterdocs-icons', 'admin/btd-icon/style.css' );
751 626 }
752 627
753 628 /**
@@ -758,70 +633,26 @@
758 633 * @return void
759 634 * @since 1.0.0
760 635 */
761 636 public function scripts( $hook ) {
762 - // Classic-UI screens that should offer a "Switch to BetterDocs UI"
763 - // button: All Docs, FAQ list, FAQ groups, Product FAQ groups,
764 - // Doc Categories, Doc Tags. Maps each to the React admin page to
765 - // return to; $switch_args carries extra query args (e.g. the FAQ
766 - // Builder tab) appended to the React page URL.
767 - $switch_page = '';
768 - $switch_args = array();
769 - if ( 'edit.php' === $hook && 'docs' === get_post_type() ) {
770 - $switch_page = 'betterdocs-admin';
771 - } elseif ( 'edit.php' === $hook && 'betterdocs_faq' === get_post_type() ) {
772 - $switch_page = 'betterdocs-faq';
773 - } elseif ( 'edit-tags.php' === $hook ) {
774 - $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
775 - $taxonomy = $screen && ! empty( $screen->taxonomy )
776 - ? $screen->taxonomy
777 - : ( isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
778 - if ( 'betterdocs_faq_category' === $taxonomy ) {
779 - $switch_page = 'betterdocs-faq';
780 - } elseif ( 'betterdocs_product_faq_category' === $taxonomy ) {
781 - // Product FAQ groups live on the FAQ Builder's WooCommerce tab.
782 - $switch_page = 'betterdocs-faq';
783 - $switch_args = array( 'faq_tab' => 'woocommerce' );
784 - } elseif ( 'doc_category' === $taxonomy ) {
785 - $switch_page = 'betterdocs-doc-categories';
786 - } elseif ( 'doc_tag' === $taxonomy ) {
787 - $switch_page = 'betterdocs-doc-tags';
788 - }
789 - }
790 -
791 - /**
792 - * Allow Pro/add-ons to map their own classic-UI taxonomy screens to a
793 - * React admin page for the "Switch to BetterDocs UI" button — e.g. the
794 - * Knowledge Base taxonomy, which only exists when Pro is active.
795 - *
796 - * @param string $switch_page React page slug, or '' for no switcher.
797 - * @param string $hook Current admin page hook.
798 - */
799 - $switch_page = apply_filters( 'betterdocs_classic_switch_page', $switch_page, $hook );
800 -
801 - if ( $switch_page ) {
637 + if ( ( $hook === 'edit.php' ) && get_post_type() == 'docs' ) {
802 638 $this->assets->enqueue(
803 639 'betterdocs-switcher',
804 640 'admin/js/switcher.js',
805 - array(
806 - 'jquery',
807 - )
641 + [
642 + 'jquery'
643 + ]
808 644 );
809 645
810 646 $this->assets->localize(
811 647 'betterdocs-switcher',
812 648 'betterdocsSwitcher',
813 - array(
649 + [
814 650 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
815 - 'page' => $switch_page,
816 - 'url' => add_query_arg(
817 - array_merge( array( 'page' => $switch_page ), $switch_args ),
818 - admin_url( 'admin.php' )
819 - ),
820 651 'site_address' => get_bloginfo( 'url' ),
821 652 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
822 - 'betterdocs_pro_version' => betterdocs()->pro_version(),
823 - )
653 + 'betterdocs_pro_version' => betterdocs()->pro_version()
654 + ]
824 655 );
825 656
826 657 return;
827 658 }
@@ -831,121 +662,82 @@
831 662 if ( ! betterdocs()->is_betterdocs_screen( $hook ) ) {
832 663 return;
833 664 }
834 665
835 - wp_enqueue_media(); // load early to fix problems with media upload issues on settings for WordPress 6.0.9
666 + wp_enqueue_media(); // load early to fix problems with media upload issues on settings for wordpress 6.0.9
836 667 $this->assets->register( 'betterdocs-admin', 'admin/js/dashboard.js' );
837 668
838 669 $saved_settings = get_option( 'betterdocs_settings', false );
839 - $dark_mode = $this->is_dark_mode();
670 + $dark_mode = $saved_settings['dark_mode'] ?? false;
671 + $dark_mode = ! empty( $dark_mode ) && boolval( $dark_mode );
840 672 $this->assets->localize(
841 673 'betterdocs-admin',
842 674 'betterdocs_admin',
843 - array(
675 + [
844 676 'ajaxurl' => admin_url( 'admin-ajax.php' ),
845 677 'doc_cat_order_nonce' => wp_create_nonce( 'doc_cat_order_nonce' ),
846 678 'knowledge_base_order_nonce' => wp_create_nonce( 'knowledge_base_order_nonce' ),
847 - 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- pagination read from URL.
848 - 'per_page_id' => 'edit_doc_category_per_page',
849 - 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
850 - 'dark_mode' => $dark_mode,
851 - 'kb_is_empty' => $this->kb_is_empty(),
852 - 'text' => __( 'Copied!', 'betterdocs' ),
853 - 'test_report' => __( 'Test Report!', 'betterdocs' ),
854 - 'sending' => __( 'Sending...', 'betterdocs' ),
855 - 'dir_url' => BETTERDOCS_ABSURL,
856 - 'rest_url' => esc_url_raw( rest_url() ),
857 - 'free_version' => betterdocs()->version,
858 - 'generate_data_url' => get_rest_url( null, '/betterdocs/v1/create-sample-docs' ),
859 - 'ai_sample_docs' => array(
860 - 'enabled' => (bool) betterdocs()->settings->get( 'enable_ai_sample_docs', true ),
861 - 'rest_base' => esc_url_raw( get_rest_url( null, '/betterdocs/v1/sample-docs' ) ),
862 - ),
863 - 'nonce' => wp_create_nonce( 'wp_rest' ),
864 - 'sync_nonce' => wp_create_nonce( 'ai_chatbot_embed' ),
865 - 'count_all_docs' => array_sum( (array) wp_count_posts( 'docs' ) ),
866 - 'count_all_faq' => array_sum( (array) wp_count_posts( 'betterdocs_faq' ) ),
867 - 'faq_order' => get_option( 'betterdocs_faq_order', 'default' ),
868 - 'count_new_docs' => $this->get_not_synced_docs_count(),
869 - 'admin_url' => admin_url(),
870 - 'ia_preview' => betterdocs()->settings->get( 'ia_enable_preview', false ),
871 - 'multiple_kb' => betterdocs()->settings->get( 'multiple_kb' ),
872 - 'previewMode' => betterdocs()->settings->get( 'ia_enable_preview', false ),
873 - 'dashboard_mode' => get_option( 'dashboard_mode' ),
874 - 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
875 - 'betterdocs_pro_version' => betterdocs()->pro_version(),
876 - 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ),
877 - 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ),
878 - 'api_docs_teaser' => betterdocs()->show_api_docs_teaser(),
879 - 'is_woocommerce_active' => class_exists( 'WooCommerce' ),
880 - 'total_doc_category_terms' => wp_count_terms( 'doc_category' ),
881 - 'current_admin_language' => Helper::get_current_admin_language(),
882 - 'is_multilingual' => Helper::is_multilingual_active(),
883 - 'languages' => Helper::get_admin_languages(),
884 - /**
885 - * MCP page bootstrap. `abilities_api_available` decides whether the
886 - * page offers a connection at all: without the Abilities API there
887 - * is no tool catalog, so an AI client would connect and find
888 - * nothing. `enabled` is only the initial paint — the toggle owns
889 - * the value from then on.
890 - *
891 - * @since 4.9.0
892 - */
893 - 'mcp' => array(
894 - 'abilities_api_available' => function_exists( 'wp_register_ability' ),
895 - 'enabled' => (bool) betterdocs()->settings->get( 'enable_mcp', false ),
896 - 'rest' => 'betterdocs/v1',
897 - ),
898 - )
679 + 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Missing
680 + 'per_page_id' => 'edit_doc_category_per_page',
681 + 'menu_title' => __( 'Switch to BetterDocs UI', 'betterdocs' ),
682 + 'dark_mode' => $dark_mode,
683 + 'text' => __( 'Copied!', 'betterdocs' ),
684 + 'test_report' => __( 'Test Report!', 'betterdocs' ),
685 + 'sending' => __( 'Sending...', 'betterdocs' ),
686 + 'dir_url' => BETTERDOCS_ABSURL,
687 + 'rest_url' => esc_url_raw( rest_url() ),
688 + 'free_version' => betterdocs()->version,
689 + 'generate_data_url' => get_rest_url( null, '/betterdocs/v1/create-sample-docs' ),
690 + 'nonce' => wp_create_nonce( 'wp_rest' ),
691 + 'sync_nonce' => wp_create_nonce( 'ai_chatbot_embed' ),
692 + 'count_all_docs' => array_sum((array) wp_count_posts('docs')),
693 + 'count_all_faq' => array_sum((array) wp_count_posts('betterdocs_faq')),
694 + 'count_new_docs' => count(get_option('saved_docs_post_ids', [])) + count(get_option('betterdocs_ai_chatbot_error_posts', [])),
695 + 'admin_url' => admin_url(),
696 + 'ia_preview' => betterdocs()->settings->get( 'ia_enable_preview', false ),
697 + 'multiple_kb' => betterdocs()->settings->get( 'multiple_kb' ),
698 + 'previewMode' => betterdocs()->settings->get( 'ia_enable_preview', false ),
699 + 'dashboard_mode' => get_option( 'dashboard_mode' ),
700 + 'betterdocs_pro_plugin' => betterdocs()->is_pro_active(),
701 + 'betterdocs_pro_version' => betterdocs()->pro_version(),
702 + 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ),
703 + 'disabled_embed_model_option' => get_option('disabled_embed_model_option'),
704 + 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ),
705 + 'total_doc_category_terms' => wp_count_terms( 'doc_category')
706 + ]
899 707 );
900 708
901 709 // If wp-date (which includes moment.js) is not registered, enqueue your custom moment.js
902 710 if ( ! wp_script_is( 'wp-date', 'registered' ) ) {
903 - $this->assets->enqueue( 'moment', 'vendor/js/moment.min.js', array() );
711 + $this->assets->enqueue( 'moment', 'vendor/js/moment.min.js', [] );
904 712 }
905 713 wp_enqueue_script( 'betterdocs-admin' );
906 714
907 715 /**
908 - * Duplicate Codes Need to Be Removed From Here Onwards
909 - */
716 + * Duplicate Codes Need to Be Removed From Here Onwards
717 + */
910 718
911 - // FAQ Builder Related Localization
719 + //FAQ Builder Related Localization
912 720 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/css/faq.css' );
913 721 betterdocs()->assets->enqueue( 'betterdocs-admin-faq', 'admin/js/faq.js' );
914 722
915 - // Load the classic editor (TinyMCE + QuickTags) so the FAQ rich-text editor can mount via wp.editor.initialize().
916 - if ( function_exists( 'wp_enqueue_editor' ) ) {
917 - wp_enqueue_editor();
918 - }
919 - if ( function_exists( 'wp_enqueue_media' ) ) {
920 - wp_enqueue_media();
921 - }
922 -
923 723 // removing emoji support
924 724 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
925 725 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
926 726
927 - // Get settings and remove unnecessary keys
928 - $betterdocs_settings = get_option( 'betterdocs_settings', false );
929 - if ( is_array( $betterdocs_settings ) && ! current_user_can( 'edit_docs_settings' ) ) {
930 - foreach ( Settings::sensitive_api_key_fields() as $sensitive_key ) {
931 - unset( $betterdocs_settings[ $sensitive_key ] );
932 - }
933 - }
934 -
935 727 betterdocs()->assets->localize(
936 728 'betterdocs-admin-faq',
937 729 'betterdocsFaq',
938 - array(
730 + [
939 731 'dir_url' => BETTERDOCS_ABSURL,
940 732 'rest_url' => esc_url_raw( rest_url() ),
941 733 'free_version' => betterdocs()->version,
942 734 'nonce' => wp_create_nonce( 'wp_rest' ),
943 - 'betterdocs_settings' => $betterdocs_settings,
944 - )
735 + 'betterdocs_settings' => get_option( 'betterdocs_settings', false )
736 + ]
945 737 );
946 738
947 - // Glossaries Related Localization
739 + //Glossaries Related Localization
948 740 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/css/faq.css' );
949 741
950 742 betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/js/glossaries.js' );
951 743
@@ -951,15 +743,15 @@
951 743
952 744 betterdocs()->assets->localize(
953 745 'betterdocs-admin-glossaries',
954 746 'betterdocsGlossary',
955 - array(
747 + [
956 748 'dir_url' => BETTERDOCS_ABSURL,
957 749 'rest_url' => esc_url_raw( rest_url() ),
958 750 'free_version' => betterdocs()->version,
959 751 'nonce' => wp_create_nonce( 'wp_rest' ),
960 - 'betterdocs_settings' => $betterdocs_settings,
961 - )
752 + 'betterdocs_settings' => get_option( 'betterdocs_settings', false )
753 + ]
962 754 );
963 755 }
964 756
965 757 /**
@@ -968,31 +760,31 @@
968 760 * @return void
969 761 * @since 1.0.0
970 762 */
971 763 public function header( $admin_tab_name ) {
972 - $quick_links = array(
764 + $quick_links = [
973 765 'switch_view' => sprintf(
974 766 '<a href="%s" class="betterdocs-button betterdocs-button-secondary">%s</a>',
975 767 add_query_arg(
976 - array(
768 + [
977 769 'post_type' => 'docs',
978 - 'bdocs_view' => 'classic',
979 - ),
770 + 'bdocs_view' => 'classic'
771 + ],
980 772 'edit.php'
981 773 ),
982 774 __( 'Switch to Classic UI', 'betterdocs' )
983 775 ),
984 - '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' ) ),
985 - );
776 + 'add_new_doc' => sprintf( '<a href="%s" class="betterdocs-button betterdocs-button-primary">%s</a>', add_query_arg( [ 'post_type' => 'docs' ], 'post-new.php' ), __( 'Add New Doc', 'betterdocs' ) )
777 + ];
986 778
987 779 $quick_links = apply_filters( 'betterdocs_quick_links', $quick_links );
988 780
989 781 betterdocs()->views->get(
990 782 'admin/header',
991 - array(
783 + [
992 784 'quick_links' => $quick_links,
993 - 'active_tab' => $admin_tab_name,
994 - )
785 + 'active_tab' => $admin_tab_name
786 + ]
995 787 );
996 788 }
997 789
998 790 /**
@@ -1001,17 +793,17 @@
1001 793 * @return void
1002 794 * @since 1.0.0
1003 795 */
1004 796 public function menus() {
1005 - $default_args = array(
797 + $default_args = [
1006 798 'page_title' => 'BetterDocs',
1007 799 'menu_title' => 'BetterDocs',
1008 - 'capability' => 'edit_docs', // Unified capability
800 + 'capability' => 'edit_docs', // Unified capability
1009 801 'menu_slug' => $this->slug,
1010 - 'callback' => array( $this, 'output' ),
802 + 'callback' => [ $this, 'output' ],
1011 803 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
1012 - 'position' => 5,
1013 - );
804 + 'position' => 5
805 + ];
1014 806
1015 807 $_menu_position = 5;
1016 808 global $submenu;
1017 809
@@ -1017,29 +809,24 @@
1017 809
1018 810 // Always register both UI endpoints
1019 811 $this->register_modern_ui_fallback();
1020 812
1021 - // The one-time MCP discovery badge (ADR-063). Decided once, here, and
1022 - // remembered for `mcp_badge_styles()`: the pill's markup and the pill's
1023 - // stylesheet have to be printed on the same requests as each other.
1024 - $this->mcp_badge = self::should_flag_mcp();
1025 -
1026 813 foreach ( $this->menu_list() as $key => $value ) {
1027 - if ( 'betterdocs' === $key ) {
814 + if ( $key === 'betterdocs' ) {
1028 815 $callable = 'add_menu_page';
1029 816 $value = wp_parse_args( $value, $default_args );
1030 817 call_user_func_array( $callable, $value );
1031 818 } else {
1032 - $is_core_page = strpos( $value['menu_slug'], '?' ) !== false;
819 + $is_core_page = strpos($value['menu_slug'], '?') !== false;
1033 820
1034 - if ( $is_core_page ) {
821 + if ($is_core_page) {
1035 822 // Add classic UI directly
1036 - $submenu[ $this->slug ][] = array(
823 + $submenu[$this->slug][] = [
1037 824 $value['menu_title'],
1038 825 $value['capability'],
1039 826 $value['menu_slug'],
1040 - $value['page_title'],
1041 - );
827 + $value['page_title']
828 + ];
1042 829 } else {
1043 830 // Add modern UI through WordPress API
1044 831 add_submenu_page(
1045 832 $this->slug,
@@ -1049,223 +836,30 @@
1049 836 $value['menu_slug'],
1050 837 $value['callback']
1051 838 );
1052 839 }
1053 - ++$_menu_position;
840 + $_menu_position++;
1054 841 }
1055 842 }
1056 -
1057 - $this->paint_mcp_badge();
1058 843 }
1059 844
1060 - /**
1061 - * Append the discovery badge to the registered menu titles.
1062 - *
1063 - * **After** registration, editing `$menu` / `$submenu` in place — the same
1064 - * shape `add_custom_classes_to_menu_items()` uses — and never by passing a
1065 - * decorated title to `add_menu_page()`. That distinction is not cosmetic:
1066 - * core stores `sanitize_title( $menu_title )` as `$admin_page_hooks[ $slug ]`
1067 - * (`wp-admin/includes/plugin.php:1397`) and builds every child page's hook
1068 - * suffix from it (`get_plugin_page_hookname()`), so markup in the parent's
1069 - * title renames `betterdocs_page_betterdocs-mcp` — and the MCP screen, whose
1070 - * asset enqueue is keyed on that exact suffix, silently loads no React
1071 - * bundle at all. Measured: the first visit came back 102,513 bytes with no
1072 - * `dashboard.js`, against 489,272 bytes once the badge had cleared.
1073 - *
1074 - * Titles are only ever appended to, never rebuilt: the menu list is filtered
1075 - * (`betterdocs_admin_menu`), so whatever a filter put in a title survives.
1076 - *
1077 - * @return void
1078 - * @since 4.9.0
1079 - */
1080 - private function paint_mcp_badge() {
1081 - if ( ! $this->mcp_badge ) {
1082 - return;
1083 - }
1084 -
1085 - global $menu, $submenu;
1086 -
1087 - if ( is_array( $menu ) ) {
1088 - foreach ( $menu as &$item ) {
1089 - if ( isset( $item[2] ) && $this->slug === $item[2] ) {
1090 - $item[0] .= self::mcp_parent_bubble();
1091 - break;
1092 - }
1093 - }
1094 - unset( $item );
1095 - }
1096 -
1097 - if ( isset( $submenu[ $this->slug ] ) && is_array( $submenu[ $this->slug ] ) ) {
1098 - foreach ( $submenu[ $this->slug ] as &$sub_item ) {
1099 - if ( isset( $sub_item[2] ) && 'betterdocs-mcp' === $sub_item[2] ) {
1100 - $sub_item[0] .= self::mcp_submenu_pill();
1101 - break;
1102 - }
1103 - }
1104 - unset( $sub_item );
1105 - }
1106 - }
1107 -
1108 - /**
1109 - * Whether the current user should see the one-time MCP discovery badge.
1110 - *
1111 - * True only for a user who can actually reach the screen and has never
1112 - * opened it. The capability is the one the MCP menu item is already
1113 - * registered with (`manage_options`) rather than a second, re-derived rule —
1114 - * so the badge can never advertise a page its reader cannot open.
1115 - *
1116 - * @return bool
1117 - * @since 4.9.0
1118 - */
1119 - public static function should_flag_mcp() {
1120 - if ( ! current_user_can( 'manage_options' ) ) {
1121 - return false;
1122 - }
1123 -
1124 - $user_id = get_current_user_id();
1125 -
1126 - if ( ! $user_id ) {
1127 - return false;
1128 - }
1129 -
1130 - // `get_user_meta( …, true )` answers '' for a key that is not there, and
1131 - // the value written is always `time()` — so an empty string is the only
1132 - // shape "never opened" takes.
1133 - return '' === get_user_meta( $user_id, self::MCP_SEEN_META, true );
1134 - }
1135 -
1136 - /**
1137 - * WordPress' own update-count bubble, for the BetterDocs parent menu item.
1138 - *
1139 - * Core's markup on purpose: the red bubble, its position and its dark-mode
1140 - * colours are already in `wp-admin`'s stylesheet, so this needs no CSS of
1141 - * ours and cannot drift from the Plugins/Updates bubbles beside it.
1142 - *
1143 - * @return string
1144 - * @since 4.9.0
1145 - */
1146 - private static function mcp_parent_bubble() {
1147 - return ' <span class="update-plugins count-1"><span class="update-count">1</span></span>';
1148 - }
1149 -
1150 - /**
1151 - * The green "New" pill for the MCP submenu item.
1152 - *
1153 - * Core has no submenu-badge markup, so this one is ours — styled by
1154 - * `mcp_badge_styles()`.
1155 - *
1156 - * @return string
1157 - * @since 4.9.0
1158 - */
1159 - private static function mcp_submenu_pill() {
1160 - return ' <span class="bd-menu-pill">' . esc_html__( 'New', 'betterdocs' ) . '</span>';
1161 - }
1162 -
1163 - /**
1164 - * Whether this request is the MCP screen being opened by someone who can
1165 - * open it.
1166 - *
1167 - * The whole of the clearing decision, in one static so it can be pinned by
1168 - * a test. It reads the **page slug** rather than an admin hook suffix
1169 - * because the suffix is derived state: core builds it from
1170 - * `sanitize_title()` of the *parent* menu title (`get_plugin_page_hookname()`),
1171 - * that title is filtered (`betterdocs_admin_menu`), and the parent slug is
1172 - * spelled two ways in this class already. `?page=betterdocs-mcp` is the one
1173 - * thing that identifies this screen on every install (ADR-065).
1174 - *
1175 - * The capability is `manage_options`, the same one the MCP menu item is
1176 - * registered with and the same one {@see self::should_flag_mcp()} gates on:
1177 - * nothing may be written for a user who cannot reach the page.
1178 - *
1179 - * @return bool
1180 - * @since 4.9.0
1181 - */
1182 - public static function is_mcp_screen_request() {
1183 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection; see mark_mcp_seen().
1184 - $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
1185 -
1186 - if ( 'betterdocs-mcp' !== $page ) {
1187 - return false;
1188 - }
1189 -
1190 - return current_user_can( 'manage_options' );
1191 - }
1192 -
1193 - /**
1194 - * Record that this user has now seen the MCP screen.
1195 - *
1196 - * Bound to `admin_init` — which fires for every admin request — and gated on
1197 - * the page slug, rather than to `load-{$hook_suffix}` for the one suffix
1198 - * `add_submenu_page()` happened to return. `admin_menu` has already run by
1199 - * the time `admin_init` fires (measured), so the badge is still painted on
1200 - * *this* request and is gone from the next admin page — that is expected and
1201 - * correct. Do not add JavaScript to strip it mid-request.
1202 - *
1203 - * **No nonce, on purpose.** A nonce protects a state change an attacker
1204 - * could make a logged-in administrator perform unknowingly. The only state
1205 - * here is "this administrator has now been shown the MCP screen once", it is
1206 - * written for the current user alone, it holds no attacker-chosen value, and
1207 - * the worst a forged request can achieve is hiding a discovery badge from
1208 - * the person it was drawn for. A nonce on a plain page view would also have
1209 - * to survive the menu link, which carries none.
1210 - *
1211 - * @return void
1212 - * @since 4.9.0
1213 - */
1214 - public function mark_mcp_seen() {
1215 - if ( ! self::is_mcp_screen_request() ) {
1216 - return;
1217 - }
1218 -
1219 - $user_id = get_current_user_id();
1220 -
1221 - if ( ! $user_id ) {
1222 - return;
1223 - }
1224 -
1225 - update_user_meta( $user_id, self::MCP_SEEN_META, time() );
1226 - }
1227 -
1228 - /**
1229 - * The handful of declarations the "New" pill needs, inline, and only while
1230 - * it is being shown.
1231 - *
1232 - * The menu is read from the WordPress Dashboard, and `styles()` above
1233 - * early-returns on non-BetterDocs screens — so `admin/css/dashboard.css` is
1234 - * not loaded where this pill is seen. Loading the whole BetterDocs admin
1235 - * stylesheet globally, or shipping a stylesheet file for nine declarations,
1236 - * both cost far more than printing them here. The accent is written out
1237 - * rather than taken from `--base-color-700`: that token lives in
1238 - * `dashboard.css`, which is exactly the file that is not loaded here.
1239 - *
1240 - * @return void
1241 - * @since 4.9.0
1242 - */
1243 - public function mcp_badge_styles() {
1244 - if ( ! $this->mcp_badge ) {
1245 - return;
1246 - }
1247 -
1248 - echo '<style id="betterdocs-menu-pill">#adminmenu .bd-menu-pill{display:inline-block;background:#00b884;color:#fff;font-size:10px;text-transform:uppercase;line-height:1.6;padding:1px 6px;margin-left:6px;border-radius:9px;}</style>' . "\n";
1249 - }
1250 -
1251 845 private function register_modern_ui_fallback() {
1252 846 // Add the submenu with valid parent slug
1253 847 add_submenu_page(
1254 848 'betterdocs', // Valid parent slug
1255 - __( 'All Docs', 'betterdocs' ),
849 + __('All Docs', 'betterdocs'),
1256 850 '', // Empty menu title hides it
1257 851 'edit_docs',
1258 852 'betterdocs-admin',
1259 - array( $this, 'output' )
853 + [ $this, 'output' ]
1260 854 );
1261 855
1262 856 // Hide the menu item from appearing in the admin sidebar
1263 857 global $submenu;
1264 - if ( isset( $submenu['betterdocs'] ) ) {
1265 - foreach ( $submenu['betterdocs'] as $key => $item ) {
1266 - if ( 'betterdocs-admin' === $item[2] ) {
1267 - unset( $submenu['betterdocs'][ $key ] );
858 + if (isset($submenu['betterdocs'])) {
859 + foreach ($submenu['betterdocs'] as $key => $item) {
860 + if ($item[2] === 'betterdocs-admin') {
861 + unset($submenu['betterdocs'][$key]);
1268 862 break;
1269 863 }
1270 864 }
1271 865 }
@@ -1278,17 +872,17 @@
1278 872 * @since 1.0.0
1279 873 */
1280 874 public function output() {
1281 875 if ( betterdocs()->is_pro_active()
1282 - && version_compare( betterdocs()->pro_version(), '3.3.4', '<=' )
1283 - && get_current_screen()->id == 'betterdocs_page_betterdocs-analytics' ) {
876 + && version_compare( betterdocs()->pro_version(), '3.3.4', '<=' )
877 + && get_current_screen()->id == 'betterdocs_page_betterdocs-analytics' ) {
1284 878 betterdocs_pro()->views->get( 'admin/analytics-pro' );
1285 879 } else {
1286 880 betterdocs()->views->get(
1287 881 'admin/main',
1288 - array(
1289 - 'admin_ui' => 'dnd',
1290 - )
882 + [
883 + 'admin_ui' => 'dnd'
884 + ]
1291 885 );
1292 886 }
1293 887 }
1294 888
@@ -1301,10 +895,11 @@
1301 895 * @param array $callback
1302 896 *
1303 897 * @return array
1304 898 * @since 2.5.0
899 + *
1305 900 */
1306 - private function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = array() ) {
901 + private function normalize_menu( $title, $slug, $cap = 'edit_docs', $callback = null, $optional = [] ) {
1307 902 return Helper::normalize_menu( $title, $slug, $cap, $callback, $optional );
1308 903 }
1309 904
1310 905 /**
@@ -1313,34 +908,34 @@
1313 908 * @return array
1314 909 * @since 1.0.0
1315 910 */
1316 911 private function menu_list() {
1317 - $parent_slug = array();
912 + $parent_slug = [];
1318 913
1319 - $betterdocs_admin_pages = array(
1320 - 'betterdocs' => array(
914 + $betterdocs_admin_pages = [
915 + 'betterdocs' => [
1321 916 'menu_slug' => $this->slug,
1322 917 'page_title' => 'BetterDocs',
1323 918 'menu_title' => 'BetterDocs',
1324 919 'capability' => 'edit_docs',
1325 - 'callback' => array( $this, 'output' ),
920 + 'callback' => [ $this, 'output' ],
1326 921 'icon_url' => betterdocs()->assets->icon( 'betterdocs-icon-white.svg', true ),
1327 - 'position' => 5,
1328 - ),
1329 - 'dashboard' => $this->normalize_menu(
922 + 'position' => 5
923 + ],
924 + 'dashboard' => $this->normalize_menu(
1330 925 __( 'Dashboard', 'betterdocs' ),
1331 926 'betterdocs-dashboard',
1332 927 'edit_docs',
1333 - array(
928 + [
1334 929 $this,
1335 - 'output',
1336 - )
930 + 'output'
931 + ]
1337 932 ),
1338 933 'all_docs' => $this->normalize_menu(
1339 934 __( 'All Docs', 'betterdocs' ),
1340 935 $this->ui_slug(),
1341 936 'edit_docs',
1342 - array( $this, 'output' ),
937 + [ $this, 'output' ],
1343 938 $parent_slug
1344 939 ),
1345 940 'add_new' => $this->normalize_menu(
1346 941 __( 'Add New', 'betterdocs' ),
@@ -1347,48 +942,34 @@
1347 942 'post-new.php?post_type=docs'
1348 943 ),
1349 944 'categories' => $this->normalize_menu(
1350 945 __( 'Categories', 'betterdocs' ),
1351 - 'betterdocs-doc-categories',
1352 - 'manage_doc_terms',
1353 - array( $this, 'output' ),
1354 - $parent_slug
946 + 'edit-tags.php?taxonomy=doc_category&post_type=docs',
947 + 'manage_doc_terms'
1355 948 ),
1356 949 'tags' => $this->normalize_menu(
1357 950 __( 'Tags', 'betterdocs' ),
1358 - 'betterdocs-doc-tags',
1359 - 'manage_doc_terms',
1360 - array( $this, 'output' ),
1361 - $parent_slug
951 + 'edit-tags.php?taxonomy=doc_tag&post_type=docs',
952 + 'manage_doc_terms'
1362 953 ),
1363 954 'settings' => $this->normalize_menu(
1364 955 __( 'Settings', 'betterdocs' ),
1365 956 'betterdocs-settings',
1366 957 'edit_docs_settings',
1367 - array(
958 + [
1368 959 $this,
1369 - 'output',
1370 - ),
960 + 'output'
961 + ],
1371 962 $parent_slug
1372 963 ),
1373 - 'mcp' => $this->normalize_menu(
1374 - __( 'MCP', 'betterdocs' ),
1375 - 'betterdocs-mcp',
1376 - 'manage_options',
1377 - array(
1378 - $this,
1379 - 'output',
1380 - ),
1381 - $parent_slug
1382 - ),
1383 964 'analytics' => $this->normalize_menu(
1384 965 __( 'Analytics', 'betterdocs' ),
1385 966 'betterdocs-analytics',
1386 967 'read_docs_analytics',
1387 - array(
968 + [
1388 969 $this,
1389 - 'output',
1390 - ),
970 + 'output'
971 + ],
1391 972 $parent_slug
1392 973 ),
1393 974 'faq' => $this->normalize_menu(
1394 975 __( 'FAQ Builder', 'betterdocs' ),
@@ -1393,15 +974,15 @@
1393 974 'faq' => $this->normalize_menu(
1394 975 __( 'FAQ Builder', 'betterdocs' ),
1395 976 'betterdocs-faq',
1396 977 'read_faq_builder',
1397 - array(
978 + [
1398 979 $this,
1399 - 'output',
1400 - ),
980 + 'output'
981 + ],
1401 982 $parent_slug
1402 - ),
1403 - );
983 + )
984 + ];
1404 985
1405 986 if ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'enable_glossaries' ) == true ) {
1406 987 $betterdocs_admin_pages['glossaries'] = $this->normalize_menu(
1407 988 __( 'Glossaries', 'betterdocs' ),
@@ -1406,66 +987,50 @@
1406 987 $betterdocs_admin_pages['glossaries'] = $this->normalize_menu(
1407 988 __( 'Glossaries', 'betterdocs' ),
1408 989 'betterdocs-glossaries',
1409 990 'read_docs_analytics',
1410 - array(
991 + [
1411 992 $this,
1412 - 'output',
1413 - ),
993 + 'output'
994 + ],
1414 995 $parent_slug
1415 996 );
1416 997 }
1417 998
1418 - // API Docs ships in Pro, which overwrites this same key in place — declaring
1419 - // the slot here is what keeps the item in this position. Without Pro it
1420 - // holds Free's locked teaser instead.
1421 - if ( betterdocs()->show_api_docs_teaser() || betterdocs()->has_api_docs() ) {
1422 - $betterdocs_admin_pages['api_docs'] = $this->normalize_menu(
1423 - __( 'API Docs', 'betterdocs' ),
1424 - 'betterdocs-api-docs',
1425 - apply_filters( 'betterdocs_api_ref_capability', 'manage_options' ),
1426 - array(
999 +
1000 + if (!betterdocs()->is_chatbot_active()) {
1001 + $betterdocs_admin_pages['ai_chatbot'] = $this->normalize_menu(
1002 + __('AI Chatbot', 'betterdocs'),
1003 + 'betterdocs-ai-chatbot',
1004 + 'edit_docs_settings',
1005 + [
1427 1006 $this,
1428 - 'output',
1429 - ),
1430 - $parent_slug
1431 - );
1432 - }
1007 + 'output'
1008 + ],
1009 + $parent_slug
1010 + );
1011 + }
1433 1012
1434 - if ( ! betterdocs()->is_chatbot_active() ) {
1435 - $betterdocs_admin_pages['ai_chatbot'] = $this->normalize_menu(
1436 - __( 'AI Chatbot', 'betterdocs' ),
1437 - 'betterdocs-ai-chatbot',
1438 - 'edit_docs_settings',
1439 - array(
1440 - $this,
1441 - 'output',
1442 - ),
1443 - $parent_slug
1444 - );
1445 - }
1013 + return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, [$this, 'output'], $parent_slug);
1446 1014
1447 - return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, array( $this, 'output' ), $parent_slug );
1448 - }
1015 + }
1449 1016
1450 1017 public function add_custom_classes_to_menu_items() {
1451 1018 global $menu, $submenu;
1452 1019
1453 - $menu_items = array(
1020 + $menu_items = [
1454 1021 'betterdocs' => 'betterdocs',
1455 1022 'betterdocs_page_all_docs' => 'betterdocs-all-docs',
1456 1023 'betterdocs_page_add_new' => 'betterdocs-add-new',
1457 - 'betterdocs-doc-categories' => 'betterdocs-categories',
1458 - 'betterdocs-doc-tags' => 'betterdocs-tags',
1024 + 'edit-tags.php?taxonomy=doc_category&post_type=docs' => 'betterdocs-categories',
1025 + 'edit-tags.php?taxonomy=doc_tag&post_type=docs' => 'betterdocs-tags',
1459 1026 'betterdocs-settings' => 'betterdocs-settings',
1460 - 'betterdocs-mcp' => 'betterdocs-mcp',
1461 1027 'betterdocs-analytics' => 'betterdocs-analytics',
1462 1028 'betterdocs-faq' => 'betterdocs-faq',
1463 1029 'betterdocs-glossaries' => 'betterdocs-glossaries',
1464 1030 'betterdocs-ai-chatbot' => 'betterdocs-ai-chatbot',
1465 - 'betterdocs-api-docs' => 'betterdocs-api-docs',
1466 - 'edit-tags.php?taxonomy=knowledge_base&post_type=docs' => 'betterdocs-multiplekb',
1467 - );
1031 + 'edit-tags.php?taxonomy=knowledge_base&post_type=docs' => 'betterdocs-multiplekb'
1032 + ];
1468 1033
1469 1034 foreach ( $menu as &$item ) {
1470 1035 if ( isset( $menu_items[ $item[2] ] ) ) {
1471 1036 if ( ! isset( $item[4] ) ) {
@@ -1495,12 +1060,12 @@
1495 1060 $menus['quick_setup'] = $this->normalize_menu(
1496 1061 __( 'Quick Setup', 'betterdocs' ),
1497 1062 'betterdocs-setup',
1498 1063 'delete_users',
1499 - array(
1064 + [
1500 1065 $this->container->get( SetupWizard::class ),
1501 - 'views',
1502 - )
1066 + 'views'
1067 + ]
1503 1068 );
1504 1069 }
1505 1070
1506 1071 return $menus;
@@ -1508,12 +1073,8 @@
1508 1073
1509 1074 public function insert_plugin_links( $links ) {
1510 1075 $links[] = '<a href="admin.php?page=betterdocs-settings">' . __( 'Settings', 'betterdocs' ) . '</a>';
1511 1076
1512 - if ( ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) {
1513 - $links[] = '<a href="https://betterdocs.co/upgrade-to-pro-plugins-wp" target="_blank" style="color: #000; font-weight: bold;">' . __( 'Upgrade to Pro', 'betterdocs' ) . '</a>';
1514 - }
1515 -
1516 1077 return $links;
1517 1078 }
1518 1079
1519 1080 public function toolbar_menu( $admin_bar ) {
@@ -1540,22 +1101,17 @@
1540 1101 }
1541 1102
1542 1103 $slug = $this->settings->get( 'encyclopedia_root_slug' );
1543 1104
1544 - global $wp_rewrite;
1545 - if ( $wp_rewrite->using_index_permalinks() ) {
1546 - $slug = $wp_rewrite->index . '/' . $slug;
1547 - }
1548 -
1549 1105 $encyclopedia_url = home_url( $slug );
1550 1106
1551 1107 $admin_bar->add_node(
1552 - array(
1108 + [
1553 1109 'parent' => 'site-name',
1554 1110 'id' => 'view-docs',
1555 1111 'title' => __( 'Visit Documentation', 'betterdocs' ),
1556 - 'href' => $docs_url,
1557 - )
1112 + 'href' => $docs_url
1113 + ]
1558 1114 );
1559 1115
1560 1116 $is_enable_encyclopedia = betterdocs()->settings->get( 'enable_encyclopedia' );
1561 1117
@@ -1560,14 +1116,14 @@
1560 1116 $is_enable_encyclopedia = betterdocs()->settings->get( 'enable_encyclopedia' );
1561 1117
1562 1118 if ( $is_enable_encyclopedia && betterdocs()->is_pro_active() ) {
1563 1119 $admin_bar->add_node(
1564 - array(
1120 + [
1565 1121 'parent' => 'site-name',
1566 1122 'id' => 'view-encyclopedia',
1567 1123 'title' => __( 'Visit Encyclopedia', 'betterdocs' ),
1568 - 'href' => $encyclopedia_url,
1569 - )
1124 + 'href' => $encyclopedia_url
1125 + ]
1570 1126 );
1571 1127 }
1572 1128 }
1573 1129
@@ -1574,20 +1130,14 @@
1574 1130 /**
1575 1131 * Save last visited admin ui
1576 1132 *
1577 1133 * @since 3.0.1
1134 + *
1578 1135 */
1579 1136 public function save_admin_page() {
1580 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1581 - $post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
1582 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1583 - $bdocs_view = isset( $_GET['bdocs_view'] ) ? sanitize_text_field( wp_unslash( $_GET['bdocs_view'] ) ) : '';
1584 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection.
1585 - $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
1586 -
1587 - if ( 'docs' === $post_type && 'classic' === $bdocs_view ) {
1137 + if ( isset( $_GET['post_type'] ) && $_GET['post_type'] === 'docs' && isset( $_GET['bdocs_view'] ) && $_GET['bdocs_view'] === 'classic' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1588 1138 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'classic_ui' );
1589 - } elseif ( 'betterdocs-admin' === $page ) {
1139 + } elseif ( isset( $_GET['page'] ) && $_GET['page'] === 'betterdocs-admin' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1590 1140 update_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', 'modern_ui' );
1591 1141 }
1592 1142 }
1593 1143
@@ -1597,20 +1147,18 @@
1597 1147 * @return string
1598 1148 * @since 3.0.1
1599 1149 */
1600 1150 public function ui_slug() {
1601 - $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1602 - $docs_exist = get_posts(
1603 - array(
1604 - 'post_type' => 'docs',
1605 - 'post_status' => 'any',
1606 - 'numberposts' => 1,
1607 - )
1608 - );
1151 + $last_visited = get_user_meta(get_current_user_id(), 'last_visited_docs_admin_page', true);
1152 + $docs_exist = get_posts([
1153 + 'post_type' => 'docs',
1154 + 'post_status' => 'any',
1155 + 'numberposts' => 1
1156 + ]);
1609 1157
1610 - return ( 'modern_ui' === $last_visited || empty( $docs_exist ) )
1611 - ? 'betterdocs-admin'
1612 - : 'edit.php?post_type=docs&bdocs_view=classic';
1158 + return ($last_visited === 'modern_ui' || empty($docs_exist))
1159 + ? 'betterdocs-admin'
1160 + : 'edit.php?post_type=docs&bdocs_view=classic';
1613 1161 }
1614 1162
1615 1163 /**
1616 1164 * Resets a duplicate submenu in WordPress if the parent main menu and the first submenu permalink are not the same.
@@ -1620,9 +1168,9 @@
1620 1168 */
1621 1169 public function reset_submenu() {
1622 1170 global $submenu;
1623 1171
1624 - $docs = get_posts( array( 'post_type' => 'docs' ) );
1172 + $docs = get_posts( [ 'post_type' => 'docs' ] );
1625 1173 if ( count( $docs ) == 0 ) {
1626 1174 return;
1627 1175 }
1628 1176
@@ -1627,89 +1175,10 @@
1627 1175 }
1628 1176
1629 1177 $last_visited = get_user_meta( get_current_user_id(), 'last_visited_docs_admin_page', true );
1630 1178
1631 - if ( 'classic_ui' === $last_visited && isset( $submenu['betterdocs-admin'] ) && in_array( 'betterdocs-admin', $submenu['betterdocs-admin'][0] ) ) {
1179 + if ( $last_visited === 'classic_ui' && isset( $submenu['betterdocs-admin'] ) && in_array( 'betterdocs-admin', $submenu['betterdocs-admin'][0] ) ) {
1632 1180 unset( $submenu['betterdocs-admin'][0] );
1633 1181 $submenu['betterdocs-admin'] = array_values( $submenu['betterdocs-admin'] );
1634 1182 }
1635 - }
1636 -
1637 - /**
1638 - * Initialize Black Friday Pointer
1639 - *
1640 - * @return void
1641 - * @since 3.7.0
1642 - */
1643 - private function init_black_friday_pointer() {
1644 - // Only initialize if conditions are met
1645 - if ( NoticePointers::should_display_notice() ) {
1646 - new NoticePointers();
1647 - }
1648 - }
1649 -
1650 - /**
1651 - * AJAX handler for dismissing Black Friday pointer
1652 - *
1653 - * @return void
1654 - * @since 3.7.0
1655 - */
1656 - public function ajax_dismiss_black_friday_pointer() {
1657 - // Verify nonce
1658 - $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
1659 - if ( ! wp_verify_nonce( $nonce, 'betterdocs_dismiss_pointer' ) ) {
1660 - wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'betterdocs' ) ) );
1661 - }
1662 -
1663 - // Check if user has permission
1664 - if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'edit_docs' ) ) {
1665 - wp_send_json_error( array( 'message' => __( 'Permission denied', 'betterdocs' ) ) );
1666 - }
1667 -
1668 - // Get the introduction key
1669 - $introduction_key = isset( $_POST['introduction_key'] ) ? sanitize_text_field( wp_unslash( $_POST['introduction_key'] ) ) : '';
1670 -
1671 - if ( empty( $introduction_key ) ) {
1672 - wp_send_json_error( array( 'message' => __( 'Invalid introduction key', 'betterdocs' ) ) );
1673 - }
1674 -
1675 - // Set the introduction as viewed
1676 - NoticePointers::set_introduction_viewed( $introduction_key );
1677 -
1678 - // Clear the priority option so other plugins can set their priority
1679 - delete_option( '_wpdeveloper_plugin_pointer_priority' );
1680 -
1681 - wp_send_json_success( array( 'message' => __( 'Pointer dismissed successfully', 'betterdocs' ) ) );
1682 - }
1683 -
1684 - /**
1685 - * Get count of docs that are not yet synced
1686 - * Similar to Helper::get_not_synced_post_ids() in betterdocs-ai-chatbot plugin
1687 - *
1688 - * @return int
1689 - * @since 3.7.0
1690 - */
1691 - private function get_not_synced_docs_count() {
1692 - $new_post_ids = get_option( 'saved_docs_post_ids', array() );
1693 - $error_posts_data = get_option( 'betterdocs_ai_chatbot_error_posts', array() );
1694 -
1695 - // Extract post IDs from error posts (handle both old and new formats)
1696 - $error_post_ids = array();
1697 - if ( is_array( $error_posts_data ) && ! empty( $error_posts_data ) ) {
1698 - foreach ( $error_posts_data as $key => $value ) {
1699 - if ( is_numeric( $key ) && is_numeric( $value ) ) {
1700 - // Old format: numeric key with post ID as value
1701 - $error_post_ids[] = $value;
1702 - } elseif ( is_numeric( $key ) && is_array( $value ) && isset( $value['post_id'] ) ) {
1703 - // New format: post ID as key with structured data
1704 - $error_post_ids[] = $key;
1705 - } elseif ( is_numeric( $key ) ) {
1706 - // New format: post ID as key
1707 - $error_post_ids[] = $key;
1708 - }
1709 - }
1710 - }
1711 -
1712 - // Merge both arrays and remove duplicates, then count
1713 - return count( array_unique( array_merge( $new_post_ids, $error_post_ids ) ) );
1714 1183 }
1715 1184 }