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