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 +306 -3 4.6.2 → 4.9.4 View file →
@@ -10,8 +10,11 @@
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;
15 +use WPDeveloper\BetterDocs\Core\AnalyticsTracker;
16 +use WPDeveloper\BetterDocs\Core\AnalyticsRetention;
14 17 use WPDeveloper\BetterDocs\Core\BaseAPI;
15 18 use WPDeveloper\BetterDocs\Core\Install;
16 19 use WPDeveloper\BetterDocs\Core\KBMigration;
17 20 use WPDeveloper\BetterDocs\Core\Query;
@@ -24,15 +27,22 @@
24 27 use WPDeveloper\BetterDocs\Core\WriteWithAI;
25 28 use WPDeveloper\BetterDocs\Core\ArticleSummary;
26 29 use WPDeveloper\BetterDocs\Core\ArticleQualityScore;
27 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;
28 35 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
29 36 use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
30 37 use WPDeveloper\BetterDocs\Editors\Editor;
31 38 use WPDeveloper\BetterDocs\FrontEnd\FrontEnd;
39 +use WPDeveloper\BetterDocs\FrontEnd\PrintTemplate;
32 40 use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
33 41 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
34 42 use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
43 +use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
44 +use WPDeveloper\BetterDocs\Mcp\MCPManager;
35 45 use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
36 46 use WPDeveloper\BetterDocs\Utils\Database;
37 47 use WPDeveloper\BetterDocs\Utils\Enqueue;
38 48 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -123,12 +133,32 @@
123 133 * @var Analytics
124 134 */
125 135 public $analytics;
126 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 + /**
127 157 * Plugin Version
128 158 * @var string
129 159 */
130 - public $version = '4.6.2';
160 + public $version = '4.9.4';
131 161
132 162 /**
133 163 * WriteWithAI Class
134 164 * @var string
@@ -145,22 +175,55 @@
145 175 /**
146 176 * Plugin DB Version
147 177 * @var string
148 178 */
149 - public $db_version = '1.0.1';
179 + public $db_version = '1.0.3';
150 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 +
151 187 public function __construct() {
152 188 $this->define_constants();
153 189
154 190 do_action( 'betterdocs_init_before' );
191 + $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
155 192
156 193 $this->setup_container();
194 +
157 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 + /**
158 208 * Register activation and deactivation hooks
159 209 * and version updates check
160 210 */
161 211 $this->container->get( Install::class );
162 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 +
163 226 add_action( 'init', array( $this, 'initialize' ), 0 );
164 227
165 228 /**
166 229 * Initialize API
@@ -180,8 +243,13 @@
180 243 /**
181 244 * Style Handler For Parsing and Saving Styles as file.
182 245 */
183 246 ModulesStyleHandler::init();
247 +
248 + /**
249 + * Serve converted GIFs in docs as video.
250 + */
251 + \WPDeveloper\BetterDocs\Modules\GifVideo::init();
184 252 }
185 253
186 254 private function define_constants() {
187 255 $this->define( 'BETTERDOCS_VERSION', $this->version );
@@ -188,9 +256,11 @@
188 256 $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
189 257 $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
190 258 $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
191 259 $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
192 - $this->define( 'BETTERDOCS_BLOCKS_DIRECTORY', BETTERDOCS_ABSPATH . 'assets/blocks/' );
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/' );
193 263 $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
194 264 $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
195 265
196 266 /**
@@ -228,8 +298,84 @@
228 298 $builder->addDefinitions( $config );
229 299 $this->container = $builder->build();
230 300 }
231 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 +
232 378 public function initialize() {
233 379
234 380 /**
235 381 * Setup localization.
@@ -255,8 +401,23 @@
255 401 $this->database = $this->container->get( Database::class );
256 402 $this->settings = $this->container->get( Settings::class );
257 403 $this->analytics = $this->container->get( Analytics::class );
258 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 +
259 420 $this->template_helper = $this->container->get( TemplateTags::class );
260 421 $this->customizer = $this->container->get( Customizer::class );
261 422 $this->editor = $this->container->get( Editor::class );
262 423 $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
@@ -265,10 +426,23 @@
265 426 // Initialize unified metabox before individual features
266 427 $this->container->get( UnifiedMetabox::class );
267 428 $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
268 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 +
269 434 $this->container->get( Admin::class );
270 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 +
271 445 $this->container->get( ReportEmail::class );
272 446 // Usage-analytics collector. Registered here (runs on every request, incl.
273 447 // WP-Cron) rather than in Admin so its `betterdocs_insights_data` filter
274 448 // callback is present whenever the tracking payload is built.
@@ -281,8 +455,13 @@
281 455
282 456 $this->container->get( FrontEnd::class );
283 457 $this->container->get( SearchExtender::class );
284 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 +
285 464 /**
286 465 * Single-product FAQ rendering (WooCommerce). The class itself bails when
287 466 * WooCommerce is inactive or the feature is disabled.
288 467 */
@@ -334,8 +513,9 @@
334 513 if ( null == self::$_instance ) {
335 514 self::$_instance = new self();
336 515
337 516 do_action( 'betterdocs_loaded' );
517 + self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
338 518 }
339 519
340 520 return self::$_instance;
341 521 }
@@ -371,8 +551,12 @@
371 551 wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-settings' ), admin_url( 'admin.php' ) ) );
372 552 } else {
373 553 wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-setup' ), admin_url( 'admin.php' ) ) );
374 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;
375 559 }
376 560 }
377 561
378 562 /**
@@ -410,8 +594,122 @@
410 594
411 595 return false;
412 596 }
413 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 +
414 712 public function pro_version() {
415 713 if ( ! $this->is_pro_active() ) {
416 714 return false;
417 715 }
@@ -463,12 +761,15 @@
463 761 'toplevel_page_betterdocs-admin',
464 762 'admin_page_betterdocs-admin',
465 763 'betterdocs_page_betterdocs-admin',
466 764 'betterdocs_page_betterdocs-analytics',
765 + 'betterdocs_page_betterdocs-content-iq',
467 766 'betterdocs_page_betterdocs-settings',
767 + 'betterdocs_page_betterdocs-mcp',
468 768 'betterdocs_page_betterdocs-faq',
469 769 'betterdocs_page_betterdocs-glossaries',
470 770 'betterdocs_page_betterdocs-ai-chatbot',
771 + 'betterdocs_page_betterdocs-api-docs',
471 772 'betterdocs_page_betterdocs-doc-categories',
472 773 'betterdocs_page_betterdocs-doc-tags',
473 774 ) );
474 775
@@ -488,12 +789,14 @@
488 789 'toplevel_page_betterdocs-dashboard',
489 790 'admin_page_betterdocs-admin',
490 791 'betterdocs_page_betterdocs-admin',
491 792 'betterdocs_page_betterdocs-settings',
793 + 'betterdocs_page_betterdocs-mcp',
492 794 'betterdocs_page_betterdocs-analytics',
493 795 'betterdocs_page_betterdocs-faq',
494 796 'betterdocs_page_betterdocs-glossaries',
495 797 'betterdocs_page_betterdocs-ai-chatbot',
798 + 'betterdocs_page_betterdocs-api-docs',
496 799 'betterdocs_page_betterdocs-doc-categories',
497 800 'betterdocs_page_betterdocs-doc-tags',
498 801 'edit-docs'
499 802 );