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 +233 -2 4.9.0 → 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,12 +28,16 @@
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;
38 43 use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
@@ -128,12 +133,32 @@
128 133 * @var Analytics
129 134 */
130 135 public $analytics;
131 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 + /**
132 157 * Plugin Version
133 158 * @var string
134 159 */
135 - public $version = '4.9.0';
160 + public $version = '4.9.4';
136 161
137 162 /**
138 163 * WriteWithAI Class
139 164 * @var string
@@ -150,17 +175,37 @@
150 175 /**
151 176 * Plugin DB Version
152 177 * @var string
153 178 */
154 - public $db_version = '1.0.2';
179 + public $db_version = '1.0.3';
155 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 +
156 187 public function __construct() {
157 188 $this->define_constants();
158 189
159 190 do_action( 'betterdocs_init_before' );
191 + $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
160 192
161 193 $this->setup_container();
194 +
162 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 + /**
163 208 * Register activation and deactivation hooks
164 209 * and version updates check
165 210 */
166 211 $this->container->get( Install::class );
@@ -198,8 +243,13 @@
198 243 /**
199 244 * Style Handler For Parsing and Saving Styles as file.
200 245 */
201 246 ModulesStyleHandler::init();
247 +
248 + /**
249 + * Serve converted GIFs in docs as video.
250 + */
251 + \WPDeveloper\BetterDocs\Modules\GifVideo::init();
202 252 }
203 253
204 254 private function define_constants() {
205 255 $this->define( 'BETTERDOCS_VERSION', $this->version );
@@ -248,8 +298,84 @@
248 298 $builder->addDefinitions( $config );
249 299 $this->container = $builder->build();
250 300 }
251 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 +
252 378 public function initialize() {
253 379
254 380 /**
255 381 * Setup localization.
@@ -274,8 +400,19 @@
274 400 $this->admin = $this->container->get( Admin::class );
275 401 $this->database = $this->container->get( Database::class );
276 402 $this->settings = $this->container->get( Settings::class );
277 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 +
278 415 // Free analytics collectors: the frontend view/scroll tracker and the
279 416 // daily retention purge. Each self-registers its hooks on construct.
280 417 $this->container->get( AnalyticsTracker::class );
281 418 $this->container->get( AnalyticsRetention::class );
@@ -318,8 +455,13 @@
318 455
319 456 $this->container->get( FrontEnd::class );
320 457 $this->container->get( SearchExtender::class );
321 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 +
322 464 /**
323 465 * Single-product FAQ rendering (WooCommerce). The class itself bails when
324 466 * WooCommerce is inactive or the feature is disabled.
325 467 */
@@ -371,8 +513,9 @@
371 513 if ( null == self::$_instance ) {
372 514 self::$_instance = new self();
373 515
374 516 do_action( 'betterdocs_loaded' );
517 + self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
375 518 }
376 519
377 520 return self::$_instance;
378 521 }
@@ -478,8 +621,95 @@
478 621 public function show_api_docs_teaser() {
479 622 return ! $this->is_pro_active() && ! $this->has_api_docs();
480 623 }
481 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 +
482 712 public function pro_version() {
483 713 if ( ! $this->is_pro_active() ) {
484 714 return false;
485 715 }
@@ -531,8 +761,9 @@
531 761 'toplevel_page_betterdocs-admin',
532 762 'admin_page_betterdocs-admin',
533 763 'betterdocs_page_betterdocs-admin',
534 764 'betterdocs_page_betterdocs-analytics',
765 + 'betterdocs_page_betterdocs-content-iq',
535 766 'betterdocs_page_betterdocs-settings',
536 767 'betterdocs_page_betterdocs-mcp',
537 768 'betterdocs_page_betterdocs-faq',
538 769 'betterdocs_page_betterdocs-glossaries',