PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
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
betterdocs / includes / Plugin.php

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

644 lines 20.9 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.2';
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 licensing it).
490 *
491 * Glossaries ships in Free's code but is Pro-licensed, so unlike API Docs there
492 * is no separate Pro class to detect — the gate is simply Pro being active. Kept
493 * behind a filter so Pro/add-ons can flip it explicitly, mirroring
494 * `betterdocs_pro_has_api_docs`.
495 *
496 * @return bool
497 */
498 public function has_glossaries() {
499 return (bool) apply_filters( 'betterdocs_pro_has_glossaries', $this->is_pro_active() );
500 }
501
502 /**
503 * Whether Free should show its locked Glossaries teaser.
504 *
505 * Only without Pro — an active Pro either has the feature or has simply left it
506 * disabled in settings; neither case wants an upsell. Mirrors
507 * show_api_docs_teaser().
508 *
509 * @return bool
510 */
511 public function show_glossary_teaser() {
512 return ! $this->is_pro_active() && ! $this->has_glossaries();
513 }
514
515 /**
516 * Whether Pro provides the real Content Intelligence screen.
517 *
518 * Pro flips this via `betterdocs_pro_has_content_intelligence`; the
519 * class_exists() default keeps the gate correct against a Pro build that
520 * predates that filter.
521 *
522 * @return bool
523 */
524 public function has_content_intelligence() {
525 return (bool) apply_filters(
526 'betterdocs_pro_has_content_intelligence',
527 class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\ContentIntelligenceService' )
528 );
529 }
530
531 /**
532 * Whether Free should show its locked Content Intelligence teaser.
533 *
534 * Only without Pro. An older Pro has already paid, so they get nothing here —
535 * they need a plugin update, not an upsell.
536 *
537 * @return bool
538 */
539 public function show_content_intelligence_teaser() {
540 return ! $this->is_pro_active() && ! $this->has_content_intelligence();
541 }
542
543 public function pro_version() {
544 if ( ! $this->is_pro_active() ) {
545 return false;
546 }
547
548 if ( ! function_exists( 'get_plugin_data' ) ) {
549 require_once ABSPATH . 'wp-admin/includes/plugin.php';
550 }
551
552 $plugin_data = get_plugin_data( $this->pro_file() );
553
554 return $plugin_data[ 'Version' ];
555 }
556
557 /**
558 * Get all the API initialized.
559 * @return void
560 */
561 public function api_initialization() {
562 $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
563
564 if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
565 foreach ( $_api_classes as $class ) {
566 if ( '.' == $class || '..' == $class || strpos( $class, '.' ) === 0 ) {
567 continue;
568 }
569
570 $classname = basename( $class, '.php' );
571 $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
572 $_api_class = $this->container->get( $classname );
573
574 if ( $_api_class instanceof BaseAPI ) {
575 $_api_class->register();
576 }
577 }
578 }
579 }
580
581 public function is_betterdocs_screen( $hook, $admin_check = true ): bool {
582 /**
583 * Filter the list of admin screen hook suffixes treated as BetterDocs
584 * screens (controls whether the React admin bundle + styles load).
585 * Pro/add-ons can register their own React pages here, e.g. the
586 * Knowledge Base admin page.
587 *
588 * @param string[] $screens Full hook suffixes (e.g. betterdocs_page_betterdocs-foo).
589 */
590 $screens = apply_filters( 'betterdocs_admin_screens', array(
591 'toplevel_page_betterdocs-dashboard',
592 'toplevel_page_betterdocs-admin',
593 'admin_page_betterdocs-admin',
594 'betterdocs_page_betterdocs-admin',
595 'betterdocs_page_betterdocs-analytics',
596 'betterdocs_page_betterdocs-content-iq',
597 'betterdocs_page_betterdocs-settings',
598 'betterdocs_page_betterdocs-mcp',
599 'betterdocs_page_betterdocs-faq',
600 'betterdocs_page_betterdocs-glossaries',
601 'betterdocs_page_betterdocs-ai-chatbot',
602 'betterdocs_page_betterdocs-api-docs',
603 'betterdocs_page_betterdocs-doc-categories',
604 'betterdocs_page_betterdocs-doc-tags',
605 ) );
606
607 if ( $admin_check ) {
608 if ( in_array( $hook, $screens ) ) {
609 return true;
610 }
611
612 return false;
613 }
614
615 return false;
616 }
617
618 public function get_betterdocs_screen() {
619 $registered_screens = array(
620 'toplevel_page_betterdocs-dashboard',
621 'admin_page_betterdocs-admin',
622 'betterdocs_page_betterdocs-admin',
623 'betterdocs_page_betterdocs-settings',
624 'betterdocs_page_betterdocs-mcp',
625 'betterdocs_page_betterdocs-analytics',
626 'betterdocs_page_betterdocs-faq',
627 'betterdocs_page_betterdocs-glossaries',
628 'betterdocs_page_betterdocs-ai-chatbot',
629 'betterdocs_page_betterdocs-api-docs',
630 'betterdocs_page_betterdocs-doc-categories',
631 'betterdocs_page_betterdocs-doc-tags',
632 'edit-docs'
633 );
634
635 $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
636
637 if ( in_array( $current_screen_id, $registered_screens ) ) {
638 return $current_screen_id;
639 }
640
641 return false;
642 }
643 }
644