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
← All changes | includes/Plugin.php +288 -2 4.8.1 → 4.9.4 View file →
@@ -10,8 +10,9 @@
10 10 use WPDeveloper\BetterDocs\Admin\Customizer\Customizer;
11 11 use WPDeveloper\BetterDocs\Admin\HelpScoutMigration;
12 12 use WPDeveloper\BetterDocs\Admin\ReportEmail;
13 13 use WPDeveloper\BetterDocs\Core\Admin;
14 +use WPDeveloper\BetterDocs\Core\AIActions;
14 15 use WPDeveloper\BetterDocs\Core\AnalyticsTracker;
15 16 use WPDeveloper\BetterDocs\Core\AnalyticsRetention;
16 17 use WPDeveloper\BetterDocs\Core\BaseAPI;
17 18 use WPDeveloper\BetterDocs\Core\Install;
@@ -27,15 +28,21 @@
27 28 use WPDeveloper\BetterDocs\Core\ArticleSummary;
28 29 use WPDeveloper\BetterDocs\Core\ArticleQualityScore;
29 30 use WPDeveloper\BetterDocs\Core\UnifiedMetabox;
30 31 use WPDeveloper\BetterDocs\Core\DocsAISuite;
32 +use WPDeveloper\BetterDocs\Core\Listen;
33 +use WPDeveloper\BetterDocs\Core\MarkdownEndpoint;
34 +use WPDeveloper\BetterDocs\Core\MarkdownRenderer;
31 35 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
32 36 use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
33 37 use WPDeveloper\BetterDocs\Editors\Editor;
34 38 use WPDeveloper\BetterDocs\FrontEnd\FrontEnd;
39 +use WPDeveloper\BetterDocs\FrontEnd\PrintTemplate;
35 40 use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
36 41 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
37 42 use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
43 +use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
44 +use WPDeveloper\BetterDocs\Mcp\MCPManager;
38 45 use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
39 46 use WPDeveloper\BetterDocs\Utils\Database;
40 47 use WPDeveloper\BetterDocs\Utils\Enqueue;
41 48 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -126,12 +133,32 @@
126 133 * @var Analytics
127 134 */
128 135 public $analytics;
129 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 + /**
130 157 * Plugin Version
131 158 * @var string
132 159 */
133 - public $version = '4.8.1';
160 + public $version = '4.9.4';
134 161
135 162 /**
136 163 * WriteWithAI Class
137 164 * @var string
@@ -148,22 +175,55 @@
148 175 /**
149 176 * Plugin DB Version
150 177 * @var string
151 178 */
152 - public $db_version = '1.0.2';
179 + public $db_version = '1.0.3';
153 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 +
154 187 public function __construct() {
155 188 $this->define_constants();
156 189
157 190 do_action( 'betterdocs_init_before' );
191 + $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
158 192
159 193 $this->setup_container();
194 +
160 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 + /**
161 208 * Register activation and deactivation hooks
162 209 * and version updates check
163 210 */
164 211 $this->container->get( Install::class );
165 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 +
166 226 add_action( 'init', array( $this, 'initialize' ), 0 );
167 227
168 228 /**
169 229 * Initialize API
@@ -183,8 +243,13 @@
183 243 /**
184 244 * Style Handler For Parsing and Saving Styles as file.
185 245 */
186 246 ModulesStyleHandler::init();
247 +
248 + /**
249 + * Serve converted GIFs in docs as video.
250 + */
251 + \WPDeveloper\BetterDocs\Modules\GifVideo::init();
187 252 }
188 253
189 254 private function define_constants() {
190 255 $this->define( 'BETTERDOCS_VERSION', $this->version );
@@ -233,8 +298,84 @@
233 298 $builder->addDefinitions( $config );
234 299 $this->container = $builder->build();
235 300 }
236 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 +
237 378 public function initialize() {
238 379
239 380 /**
240 381 * Setup localization.
@@ -259,8 +400,19 @@
259 400 $this->admin = $this->container->get( Admin::class );
260 401 $this->database = $this->container->get( Database::class );
261 402 $this->settings = $this->container->get( Settings::class );
262 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 +
263 415 // Free analytics collectors: the frontend view/scroll tracker and the
264 416 // daily retention purge. Each self-registers its hooks on construct.
265 417 $this->container->get( AnalyticsTracker::class );
266 418 $this->container->get( AnalyticsRetention::class );
@@ -280,8 +432,17 @@
280 432 $this->container->get( DocsAISuite::class );
281 433
282 434 $this->container->get( Admin::class );
283 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 +
284 445 $this->container->get( ReportEmail::class );
285 446 // Usage-analytics collector. Registered here (runs on every request, incl.
286 447 // WP-Cron) rather than in Admin so its `betterdocs_insights_data` filter
287 448 // callback is present whenever the tracking payload is built.
@@ -294,8 +455,13 @@
294 455
295 456 $this->container->get( FrontEnd::class );
296 457 $this->container->get( SearchExtender::class );
297 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 +
298 464 /**
299 465 * Single-product FAQ rendering (WooCommerce). The class itself bails when
300 466 * WooCommerce is inactive or the feature is disabled.
301 467 */
@@ -347,8 +513,9 @@
347 513 if ( null == self::$_instance ) {
348 514 self::$_instance = new self();
349 515
350 516 do_action( 'betterdocs_loaded' );
517 + self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
351 518 }
352 519
353 520 return self::$_instance;
354 521 }
@@ -427,8 +594,122 @@
427 594
428 595 return false;
429 596 }
430 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 +
431 712 public function pro_version() {
432 713 if ( ! $this->is_pro_active() ) {
433 714 return false;
434 715 }
@@ -480,12 +761,15 @@
480 761 'toplevel_page_betterdocs-admin',
481 762 'admin_page_betterdocs-admin',
482 763 'betterdocs_page_betterdocs-admin',
483 764 'betterdocs_page_betterdocs-analytics',
765 + 'betterdocs_page_betterdocs-content-iq',
484 766 'betterdocs_page_betterdocs-settings',
767 + 'betterdocs_page_betterdocs-mcp',
485 768 'betterdocs_page_betterdocs-faq',
486 769 'betterdocs_page_betterdocs-glossaries',
487 770 'betterdocs_page_betterdocs-ai-chatbot',
771 + 'betterdocs_page_betterdocs-api-docs',
488 772 'betterdocs_page_betterdocs-doc-categories',
489 773 'betterdocs_page_betterdocs-doc-tags',
490 774 ) );
491 775
@@ -505,12 +789,14 @@
505 789 'toplevel_page_betterdocs-dashboard',
506 790 'admin_page_betterdocs-admin',
507 791 'betterdocs_page_betterdocs-admin',
508 792 'betterdocs_page_betterdocs-settings',
793 + 'betterdocs_page_betterdocs-mcp',
509 794 'betterdocs_page_betterdocs-analytics',
510 795 'betterdocs_page_betterdocs-faq',
511 796 'betterdocs_page_betterdocs-glossaries',
512 797 'betterdocs_page_betterdocs-ai-chatbot',
798 + 'betterdocs_page_betterdocs-api-docs',
513 799 'betterdocs_page_betterdocs-doc-categories',
514 800 'betterdocs_page_betterdocs-doc-tags',
515 801 'edit-docs'
516 802 );