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

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

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