PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 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 All 202 releases
betterdocs / includes / Plugin.php

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

813 lines 27.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit; // Exit if accessed directly
7 }
8
9 use WPDeveloper\BetterDocs\Admin\Analytics;
10 use WPDeveloper\BetterDocs\Admin\Customizer\Customizer;
11 use WPDeveloper\BetterDocs\Admin\HelpScoutMigration;
12 use WPDeveloper\BetterDocs\Admin\ReportEmail;
13 use WPDeveloper\BetterDocs\Core\Admin;
14 use WPDeveloper\BetterDocs\Core\AIActions;
15 use WPDeveloper\BetterDocs\Core\AnalyticsTracker;
16 use WPDeveloper\BetterDocs\Core\AnalyticsRetention;
17 use WPDeveloper\BetterDocs\Core\BaseAPI;
18 use WPDeveloper\BetterDocs\Core\Install;
19 use WPDeveloper\BetterDocs\Core\KBMigration;
20 use WPDeveloper\BetterDocs\Core\Query;
21 use WPDeveloper\BetterDocs\Core\Request;
22 use WPDeveloper\BetterDocs\Core\Rewrite;
23 use WPDeveloper\BetterDocs\Core\Roles;
24 use WPDeveloper\BetterDocs\Core\Scripts;
25 use WPDeveloper\BetterDocs\Core\Settings;
26 use WPDeveloper\BetterDocs\Core\ShortcodeFactory;
27 use WPDeveloper\BetterDocs\Core\WriteWithAI;
28 use WPDeveloper\BetterDocs\Core\ArticleSummary;
29 use WPDeveloper\BetterDocs\Core\ArticleQualityScore;
30 use WPDeveloper\BetterDocs\Core\UnifiedMetabox;
31 use WPDeveloper\BetterDocs\Core\DocsAISuite;
32 use WPDeveloper\BetterDocs\Core\Listen;
33 use WPDeveloper\BetterDocs\Core\MarkdownEndpoint;
34 use WPDeveloper\BetterDocs\Core\MarkdownRenderer;
35 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
36 use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
37 use WPDeveloper\BetterDocs\Editors\Editor;
38 use WPDeveloper\BetterDocs\FrontEnd\FrontEnd;
39 use WPDeveloper\BetterDocs\FrontEnd\PrintTemplate;
40 use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
41 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
42 use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
43 use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
44 use WPDeveloper\BetterDocs\Mcp\MCPManager;
45 use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
46 use WPDeveloper\BetterDocs\Utils\Database;
47 use WPDeveloper\BetterDocs\Utils\Enqueue;
48 use WPDeveloper\BetterDocs\Utils\Helper;
49 use WPDeveloper\BetterDocs\Utils\Views;
50
51 final class Plugin {
52 private static $_instance = null;
53 /**
54 * Assets manager
55 *
56 * @var Enqueue
57 */
58 public $assets;
59 /**
60 * View manager
61 *
62 * @var Views
63 */
64 public $views;
65 /**
66 * Container Manager
67 *
68 * @var Container
69 */
70 public $container;
71 /**
72 * Editor Manager
73 * @var Editor
74 */
75 public $editor;
76 /**
77 * Helper class
78 * @var Helper
79 */
80 public $helper;
81 /**
82 * KBMigration class
83 * @var KBMigration
84 */
85 public $kbmigration;
86 /**
87 * KBMigration class
88 * @var Admin
89 */
90 public $admin;
91 /**
92 * Helper class
93 * @var Database
94 */
95 public $database;
96 /**
97 * Helper class
98 * @var Settings
99 */
100 public $settings;
101 /**
102 * Helper class
103 * @var TemplateTags
104 */
105 public $template_helper;
106 /**
107 * Article Summary class
108 * @var ArticleSummary
109 */
110 public $article_summary;
111 /**
112 * Customizer class
113 * @var Customizer
114 */
115 public $customizer;
116 /**
117 * Query class
118 * @var Query
119 */
120 public $query;
121 /**
122 * Rewrite Class
123 * @var Rewrite
124 */
125 public $rewrite;
126 /**
127 * Request Class
128 * @var Request
129 */
130 public $request;
131 /**
132 * Analytics Class
133 * @var Analytics
134 */
135 public $analytics;
136 /**
137 * Markdown renderer (HTML -> Markdown for docs)
138 * @var MarkdownRenderer
139 */
140 public $markdown;
141 /**
142 * `<doc-url>.md` endpoint
143 * @var MarkdownEndpoint
144 */
145 public $markdown_endpoint;
146 /**
147 * AI Actions registry ("Copy page" split button)
148 * @var AIActions
149 */
150 public $ai_actions;
151 /**
152 * Listen ("text to audio" player in the doc meta row)
153 * @var Listen
154 */
155 public $listen;
156 /**
157 * Plugin Version
158 * @var string
159 */
160 public $version = '4.9.4';
161
162 /**
163 * WriteWithAI Class
164 * @var string
165 */
166 public $ai_autowrtie;
167 public $backgroundProccessor;
168
169 /**
170 * ArticleQualityScore Class
171 * @var ArticleQualityScore
172 */
173 public $article_quality_score;
174
175 /**
176 * Plugin DB Version
177 * @var string
178 */
179 public $db_version = '1.0.3';
180
181 /**
182 * Listeners attached to each load-time hook when it fired, keyed by hook.
183 * @var array
184 */
185 private $fired_hook_callbacks = [];
186
187 public function __construct() {
188 $this->define_constants();
189
190 do_action( 'betterdocs_init_before' );
191 $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
192
193 $this->setup_container();
194
195 /**
196 * Add-ons (Pro, AI Chatbot) hook `betterdocs_init_before` to register
197 * their container definitions and `betterdocs_loaded` to boot, and both
198 * fire while this file is loading. When the site's plugin load order puts
199 * an add-on after Free (e.g. after a migration rewrote `active_plugins`),
200 * the add-on hooks in too late — Pro then fatals resolving its own
201 * Utils\Enqueue. Catch those late listeners up once every plugin has
202 * loaded, before anything is resolved on `init`.
203 */
204 if ( ! did_action( 'plugins_loaded' ) ) {
205 add_action( 'plugins_loaded', [ $this, 'run_late_addon_listeners' ], PHP_INT_MIN );
206 }
207 /**
208 * Register activation and deactivation hooks
209 * and version updates check
210 */
211 $this->container->get( Install::class );
212
213 /**
214 * Abilities API registrar.
215 *
216 * Resolved here, at plugin load, and not from initialize(): both the
217 * bundled Abilities API and WordPress core's build their registry as a
218 * lazy singleton and fire `wp_abilities_api_init` from it, at or after
219 * `init`, the first time anything reads the registry. Our listeners
220 * therefore have to be attached before that first read, whenever it
221 * happens — and a container resolve on `init` would already be too late
222 * for a plugin that reads the registry earlier in the same hook.
223 */
224 $this->container->get( AbilitiesRegistrar::class );
225
226 add_action( 'init', array( $this, 'initialize' ), 0 );
227
228 /**
229 * Initialize API
230 */
231 add_action( 'rest_api_init', array( $this, 'api_initialization' ) );
232
233 /**
234 * For admin only
235 */
236 add_action( 'admin_init', array( $this, 'admin_init' ), 0 );
237
238 /**
239 * For AJAX only
240 */
241 $this->ajax();
242
243 /**
244 * Style Handler For Parsing and Saving Styles as file.
245 */
246 ModulesStyleHandler::init();
247
248 /**
249 * Serve converted GIFs in docs as video.
250 */
251 \WPDeveloper\BetterDocs\Modules\GifVideo::init();
252 }
253
254 private function define_constants() {
255 $this->define( 'BETTERDOCS_VERSION', $this->version );
256 $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
257 $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
258 $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
259 $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
260 // Compiled block metadata lives under assets/build/ since the Node 24 asset
261 // restructure; register_block_type() fails silently if this path is wrong.
262 $this->define( 'BETTERDOCS_BLOCKS_DIRECTORY', BETTERDOCS_ABSPATH . 'assets/build/blocks/' );
263 $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
264 $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
265
266 /**
267 * Third Party Constants
268 * @since 2.5.0
269 *
270 * WPML compatibility with Polylang
271 */
272 if ( Helper::is_plugin_active( 'polylang/polylang.php' ) ) {
273 // Polylang's documented integration constant — must use the upstream-defined name.
274 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
275 define( 'PLL_WPML_COMPAT', false );
276 }
277 }
278
279 /**
280 * Define constant if not already set.
281 *
282 * @param string $name Constant name.
283 * @param string|bool $value Constant value.
284 */
285 private function define( $name, $value ) {
286 if ( ! defined( $name ) ) {
287 // Caller passes plugin-prefixed names (BETTERDOCS_*); $name comes from a controlled internal allowlist.
288 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound
289 define( $name, $value );
290 }
291 }
292
293 public function setup_container() {
294 $config_array = require_once BETTERDOCS_ABSPATH . 'includes/config.php';
295 $config = apply_filters( 'betterdocs_container_config', $config_array );
296 $builder = new ContainerBuilder();
297
298 $builder->addDefinitions( $config );
299 $this->container = $builder->build();
300 }
301
302 /**
303 * Run the `betterdocs_init_before` and `betterdocs_loaded` listeners that
304 * missed those hooks because their plugin loaded after Free, and add the
305 * definitions they contribute through `betterdocs_container_config` to the
306 * already-built container.
307 *
308 * Only listeners attached after the original fire are run, and only the
309 * config filters they add are applied, so plugins that loaded before Free
310 * are not processed twice. Container::set() also drops any entry already
311 * resolved under the same id, so the add-on's override wins.
312 *
313 * @since 4.9.4
314 * @return void
315 */
316 public function run_late_addon_listeners() {
317 $config_filters = $this->get_hook_callbacks( 'betterdocs_container_config' );
318
319 if ( $this->run_late_listeners( 'betterdocs_init_before' ) ) {
320 $late_filters = array_diff_key( $this->get_hook_callbacks( 'betterdocs_container_config' ), $config_filters );
321
322 $config = [];
323 foreach ( $late_filters as $filter ) {
324 $config = call_user_func( $filter['function'], $config );
325 }
326
327 if ( is_array( $config ) ) {
328 foreach ( $config as $id => $definition ) {
329 $this->container->set( $id, $definition );
330 }
331 }
332 }
333
334 $this->run_late_listeners( 'betterdocs_loaded' );
335 }
336
337 /**
338 * Call the listeners attached to an already-fired hook after it fired.
339 *
340 * @param string $hook Hook name.
341 * @return bool Whether any listener ran.
342 */
343 private function run_late_listeners( $hook ) {
344 $fired = isset( $this->fired_hook_callbacks[ $hook ] ) ? $this->fired_hook_callbacks[ $hook ] : [];
345 $listeners = array_diff_key( $this->get_hook_callbacks( $hook ), $fired );
346
347 foreach ( $listeners as $listener ) {
348 // do_action() with no arguments passes a single empty string.
349 call_user_func_array( $listener['function'], array_slice( [ '' ], 0, (int) $listener['accepted_args'] ) );
350 }
351
352 return ! empty( $listeners );
353 }
354
355 /**
356 * Callbacks attached to a hook, in run order, keyed by priority and id.
357 *
358 * @param string $hook Hook name.
359 * @return array
360 */
361 private function get_hook_callbacks( $hook ) {
362 global $wp_filter;
363
364 $callbacks = [];
365 if ( empty( $wp_filter[ $hook ] ) || ! $wp_filter[ $hook ] instanceof \WP_Hook ) {
366 return $callbacks;
367 }
368
369 foreach ( $wp_filter[ $hook ]->callbacks as $priority => $items ) {
370 foreach ( $items as $id => $callback ) {
371 $callbacks[ $priority . '|' . $id ] = $callback;
372 }
373 }
374
375 return $callbacks;
376 }
377
378 public function initialize() {
379
380 /**
381 * Setup localization.
382 */
383 $this->load_plugin_textdomain();
384
385 $this->container->get( Scripts::class );
386 $this->rewrite = $this->container->get( Rewrite::class );
387 $this->request = $this->container->get( Request::class );
388 $this->query = $this->container->get( Query::class );
389
390 // Initialize background process
391 $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
392
393 $this->rewrite->init();
394 $this->request->init();
395
396 $this->assets = $this->container->get( Enqueue::class );
397 $this->views = $this->container->get( Views::class );
398 $this->helper = $this->container->get( Helper::class );
399 $this->kbmigration = $this->container->get( KBMigration::class );
400 $this->admin = $this->container->get( Admin::class );
401 $this->database = $this->container->get( Database::class );
402 $this->settings = $this->container->get( Settings::class );
403 $this->analytics = $this->container->get( Analytics::class );
404
405 // AI Actions and the Markdown endpoint. MarkdownEndpoint's constructor
406 // strips a trailing `.md` from REQUEST_URI, so it has to be built before
407 // WP::parse_request() — which this is, since initialize() IS the `init`
408 // priority-0 callback. It also has to be built AFTER $this->settings above,
409 // because that strip consults the setting.
410 $this->markdown = $this->container->get( MarkdownRenderer::class );
411 $this->markdown_endpoint = $this->container->get( MarkdownEndpoint::class );
412 $this->ai_actions = $this->container->get( AIActions::class );
413 $this->listen = $this->container->get( Listen::class );
414
415 // Free analytics collectors: the frontend view/scroll tracker and the
416 // daily retention purge. Each self-registers its hooks on construct.
417 $this->container->get( AnalyticsTracker::class );
418 $this->container->get( AnalyticsRetention::class );
419
420 $this->template_helper = $this->container->get( TemplateTags::class );
421 $this->customizer = $this->container->get( Customizer::class );
422 $this->editor = $this->container->get( Editor::class );
423 $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
424 $this->article_summary = $this->container->get( ArticleSummary::class );
425
426 // Initialize unified metabox before individual features
427 $this->container->get( UnifiedMetabox::class );
428 $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
429
430 // Editor "Suggest Categories / Tags / Glossaries" AI actions on the docs
431 // post type (gated by enable_docs_ai_suite + an OpenAI key inside the class).
432 $this->container->get( DocsAISuite::class );
433
434 $this->container->get( Admin::class );
435 $this->container->get( Roles::class );
436
437 /**
438 * MCP transport: rewrite rules, the pretty endpoint, OAuth discovery
439 * and every REST route. Resolved here rather than at plugin load
440 * (where the abilities registrar has to be) because its earliest hook
441 * is `init`, which is what this method already runs on.
442 */
443 $this->container->get( MCPManager::class );
444
445 $this->container->get( ReportEmail::class );
446 // Usage-analytics collector. Registered here (runs on every request, incl.
447 // WP-Cron) rather than in Admin so its `betterdocs_insights_data` filter
448 // callback is present whenever the tracking payload is built.
449 $this->container->get( \WPDeveloper\BetterDocs\Insights\Collector::class );
450 /**
451 * Initialize Shortcode
452 * Make sure you have listed out all shortcode in shortcode factory.
453 */
454 $this->container->get( ShortcodeFactory::class )->init();
455
456 $this->container->get( FrontEnd::class );
457 $this->container->get( SearchExtender::class );
458
459 // Running logo header / page footer for the browser's own Print command.
460 // Registered unconditionally: it covers every front-end page, including
461 // the ones that carry no BetterDocs print button.
462 $this->container->get( PrintTemplate::class );
463
464 /**
465 * Single-product FAQ rendering (WooCommerce). The class itself bails when
466 * WooCommerce is inactive or the feature is disabled.
467 */
468 $this->container->get( WooProductFAQ::class );
469
470 do_action( 'betterdocs_init' );
471
472 $this->editor->init();
473 }
474
475 /**
476 * Load plugins textdomain `betterdocs` into actions.
477 * @return void
478 */
479 public function load_plugin_textdomain( $textdomain = 'betterdocs', $plugin_file = BETTERDOCS_PLUGIN_FILE ) {
480 $locale = determine_locale();
481
482 /**
483 * Filter to adjust the BetterDocs locale to use for translations.
484 */
485 // 'plugin_locale' is a WP-core filter; using its documented name is required.
486 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
487 $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
488
489 if ( file_exists( WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' ) ) {
490 unload_textdomain( $textdomain );
491 load_textdomain( $textdomain, WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' );
492 }
493 }
494
495 /**
496 * For AJAX Only
497 * @return void
498 */
499 public function ajax() {
500 }
501
502 /**
503 * Create a plugin instance.
504 *
505 * @param mixed ...$args
506 *
507 * @return static
508 *
509 * @suppress PHP0441
510 * @since 2.5.0
511 */
512 public static function get_instance() {
513 if ( null == self::$_instance ) {
514 self::$_instance = new self();
515
516 do_action( 'betterdocs_loaded' );
517 self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
518 }
519
520 return self::$_instance;
521 }
522
523 /**
524 * Hooked with `admin_init` action.
525 * @return void
526 */
527 public function admin_init() {
528 /**
529 * Maybe Redirect
530 * for setup related settings.
531 */
532 $this->maybe_redirect();
533 }
534
535 /**
536 * Summary of maybe_redirect
537 * @return void
538 */
539 public function maybe_redirect() {
540 // Bail if no activation transient is set.
541 if ( ! $this->database->get_transient( 'betterdocs_maybe_redirect' ) ) {
542 return;
543 }
544
545 // Delete the activation transient.
546 $this->database->delete_transient( 'betterdocs_maybe_redirect' );
547
548 if ( ! is_multisite() ) {
549 $betterdocs_settings = get_option( 'betterdocs_settings' );
550 if ( $betterdocs_settings ) {
551 wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-settings' ), admin_url( 'admin.php' ) ) );
552 } else {
553 wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-setup' ), admin_url( 'admin.php' ) ) );
554 }
555 // This runs at `admin_init` priority 0. Without exiting, the rest of
556 // admin_init still executes and can mutate the very state the redirect
557 // target depends on before the browser ever follows the Location header.
558 exit;
559 }
560 }
561
562 /**
563 * Is Pro Plugin Is Installed?
564 * @return bool
565 */
566 public function is_pro_installed() {
567 return $this->helper->get_plugins( 'betterdocs-pro/betterdocs-pro.php' );
568 }
569
570 /**
571 * Is Pro Plugin Is Active?
572 * @return bool
573 */
574 public function pro_file() {
575 return WP_PLUGIN_DIR . '/betterdocs-pro/betterdocs-pro.php';
576 }
577
578 public function is_pro_active() {
579 if ( file_exists( $this->pro_file() ) ) {
580 return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
581 }
582
583 return false;
584 }
585
586 public function chatbot_file() {
587 return WP_PLUGIN_DIR . '/betterdocs-ai-chatbot/betterdocs-ai-chatbot.php';
588 }
589
590 public function is_chatbot_active() {
591 if ( file_exists( $this->chatbot_file() ) ) {
592 return $this->helper->is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
593 }
594
595 return false;
596 }
597
598 /**
599 * Whether Pro provides the real API Documentation screen.
600 *
601 * Pro flips this via `betterdocs_pro_has_api_docs`; the class_exists() default
602 * keeps the gate correct against a Pro build that predates that filter.
603 *
604 * @return bool
605 */
606 public function has_api_docs() {
607 return (bool) apply_filters(
608 'betterdocs_pro_has_api_docs',
609 class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\ApiReferences' )
610 );
611 }
612
613 /**
614 * Whether Free should show its locked API Docs teaser.
615 *
616 * Only without Pro. An older Pro has already paid, so they get nothing here —
617 * they need a plugin update, not an upsell.
618 *
619 * @return bool
620 */
621 public function show_api_docs_teaser() {
622 return ! $this->is_pro_active() && ! $this->has_api_docs();
623 }
624
625 /**
626 * Whether the real Glossaries screen is available (i.e. Pro is providing it).
627 *
628 * Glossaries now lives in Pro (moved out of Free), so — like Content
629 * Intelligence and API Docs — there is a concrete Pro class to detect. The
630 * class_exists() default keeps the gate correct against a Pro build that
631 * predates the move (that Pro is active but does NOT ship the manager, so
632 * this must be false, not simply `is_pro_active()`); the filter lets Pro/add-ons
633 * flip it explicitly.
634 *
635 * @return bool
636 */
637 public function has_glossaries() {
638 return (bool) apply_filters(
639 'betterdocs_pro_has_glossaries',
640 class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\GlossaryTaxonomy' )
641 );
642 }
643
644 /**
645 * Whether Free should render its locked Glossaries screen instead of the real
646 * manager.
647 *
648 * True whenever the Pro manager is not available — i.e. Pro is off (a genuine
649 * upsell) OR an older Pro that predates the glossaries move is active (it can no
650 * longer provide the manager, so the slot must not be left blank). Only a Pro new
651 * enough to ship GlossaryTaxonomy hides this and takes over the route.
652 *
653 * @return bool
654 */
655 public function show_glossary_teaser() {
656 return ! $this->has_glossaries();
657 }
658
659 /**
660 * Whether the locked Glossaries screen is showing because an *outdated* Pro
661 * is active (as opposed to no Pro at all).
662 *
663 * Glossaries moved into Pro, so a Pro that predates the move is active but no
664 * longer provides the manager. That user already owns Pro, so the locked
665 * screen must ask them to UPDATE Pro rather than upsell "get Pro". Mirrors the
666 * has_glossaries() capability check.
667 *
668 * @return bool
669 */
670 public function glossaries_needs_pro_update() {
671 return $this->is_pro_active() && ! $this->has_glossaries();
672 }
673
674 /**
675 * The BetterDocs Pro version that first ships the Glossaries manager, shown in
676 * the "please update Pro" copy. Filterable so the target can move with Pro.
677 *
678 * @return string
679 */
680 public function glossaries_min_pro_version() {
681 return (string) apply_filters( 'betterdocs_glossaries_min_pro_version', '4.3.1' );
682 }
683
684 /**
685 * Whether Pro provides the real Content Intelligence screen.
686 *
687 * Pro flips this via `betterdocs_pro_has_content_intelligence`; the
688 * class_exists() default keeps the gate correct against a Pro build that
689 * predates that filter.
690 *
691 * @return bool
692 */
693 public function has_content_intelligence() {
694 return (bool) apply_filters(
695 'betterdocs_pro_has_content_intelligence',
696 class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\ContentIntelligenceService' )
697 );
698 }
699
700 /**
701 * Whether Free should show its locked Content Intelligence teaser.
702 *
703 * Only without Pro. An older Pro has already paid, so they get nothing here —
704 * they need a plugin update, not an upsell.
705 *
706 * @return bool
707 */
708 public function show_content_intelligence_teaser() {
709 return ! $this->is_pro_active() && ! $this->has_content_intelligence();
710 }
711
712 public function pro_version() {
713 if ( ! $this->is_pro_active() ) {
714 return false;
715 }
716
717 if ( ! function_exists( 'get_plugin_data' ) ) {
718 require_once ABSPATH . 'wp-admin/includes/plugin.php';
719 }
720
721 $plugin_data = get_plugin_data( $this->pro_file() );
722
723 return $plugin_data[ 'Version' ];
724 }
725
726 /**
727 * Get all the API initialized.
728 * @return void
729 */
730 public function api_initialization() {
731 $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
732
733 if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
734 foreach ( $_api_classes as $class ) {
735 if ( '.' == $class || '..' == $class || strpos( $class, '.' ) === 0 ) {
736 continue;
737 }
738
739 $classname = basename( $class, '.php' );
740 $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
741 $_api_class = $this->container->get( $classname );
742
743 if ( $_api_class instanceof BaseAPI ) {
744 $_api_class->register();
745 }
746 }
747 }
748 }
749
750 public function is_betterdocs_screen( $hook, $admin_check = true ): bool {
751 /**
752 * Filter the list of admin screen hook suffixes treated as BetterDocs
753 * screens (controls whether the React admin bundle + styles load).
754 * Pro/add-ons can register their own React pages here, e.g. the
755 * Knowledge Base admin page.
756 *
757 * @param string[] $screens Full hook suffixes (e.g. betterdocs_page_betterdocs-foo).
758 */
759 $screens = apply_filters( 'betterdocs_admin_screens', array(
760 'toplevel_page_betterdocs-dashboard',
761 'toplevel_page_betterdocs-admin',
762 'admin_page_betterdocs-admin',
763 'betterdocs_page_betterdocs-admin',
764 'betterdocs_page_betterdocs-analytics',
765 'betterdocs_page_betterdocs-content-iq',
766 'betterdocs_page_betterdocs-settings',
767 'betterdocs_page_betterdocs-mcp',
768 'betterdocs_page_betterdocs-faq',
769 'betterdocs_page_betterdocs-glossaries',
770 'betterdocs_page_betterdocs-ai-chatbot',
771 'betterdocs_page_betterdocs-api-docs',
772 'betterdocs_page_betterdocs-doc-categories',
773 'betterdocs_page_betterdocs-doc-tags',
774 ) );
775
776 if ( $admin_check ) {
777 if ( in_array( $hook, $screens ) ) {
778 return true;
779 }
780
781 return false;
782 }
783
784 return false;
785 }
786
787 public function get_betterdocs_screen() {
788 $registered_screens = array(
789 'toplevel_page_betterdocs-dashboard',
790 'admin_page_betterdocs-admin',
791 'betterdocs_page_betterdocs-admin',
792 'betterdocs_page_betterdocs-settings',
793 'betterdocs_page_betterdocs-mcp',
794 'betterdocs_page_betterdocs-analytics',
795 'betterdocs_page_betterdocs-faq',
796 'betterdocs_page_betterdocs-glossaries',
797 'betterdocs_page_betterdocs-ai-chatbot',
798 'betterdocs_page_betterdocs-api-docs',
799 'betterdocs_page_betterdocs-doc-categories',
800 'betterdocs_page_betterdocs-doc-tags',
801 'edit-docs'
802 );
803
804 $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
805
806 if ( in_array( $current_screen_id, $registered_screens ) ) {
807 return $current_screen_id;
808 }
809
810 return false;
811 }
812 }
813