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 +496 -64 3.4.2 → 4.9.4 View file →
@@ -5,32 +5,49 @@
5 5 if ( ! defined( 'ABSPATH' ) ) {
6 6 exit; // Exit if accessed directly
7 7 }
8 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;
9 13 use WPDeveloper\BetterDocs\Core\Admin;
10 -use WPDeveloper\BetterDocs\Core\Query;
11 -use WPDeveloper\BetterDocs\Core\Roles;
12 -use WPDeveloper\BetterDocs\Core\WriteWithAI;
13 -use WPDeveloper\BetterDocs\Dependencies\DI\Container;
14 -use WPDeveloper\BetterDocs\Utils\Views;
14 +use WPDeveloper\BetterDocs\Core\AIActions;
15 +use WPDeveloper\BetterDocs\Core\AnalyticsTracker;
16 +use WPDeveloper\BetterDocs\Core\AnalyticsRetention;
15 17 use WPDeveloper\BetterDocs\Core\BaseAPI;
16 18 use WPDeveloper\BetterDocs\Core\Install;
19 +use WPDeveloper\BetterDocs\Core\KBMigration;
20 +use WPDeveloper\BetterDocs\Core\Query;
17 21 use WPDeveloper\BetterDocs\Core\Request;
18 22 use WPDeveloper\BetterDocs\Core\Rewrite;
23 +use WPDeveloper\BetterDocs\Core\Roles;
19 24 use WPDeveloper\BetterDocs\Core\Scripts;
20 -use WPDeveloper\BetterDocs\Utils\Helper;
21 25 use WPDeveloper\BetterDocs\Core\Settings;
22 -use WPDeveloper\BetterDocs\Core\KBMigration;
23 -use WPDeveloper\BetterDocs\Utils\Enqueue;
26 +use WPDeveloper\BetterDocs\Core\ShortcodeFactory;
27 +use WPDeveloper\BetterDocs\Core\WriteWithAI;
28 +use WPDeveloper\BetterDocs\Core\ArticleSummary;
29 +use WPDeveloper\BetterDocs\Core\ArticleQualityScore;
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;
35 +use WPDeveloper\BetterDocs\Dependencies\DI\Container;
36 +use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
24 37 use WPDeveloper\BetterDocs\Editors\Editor;
25 -use WPDeveloper\BetterDocs\Utils\Database;
26 -use WPDeveloper\BetterDocs\Admin\Analytics;
27 -use WPDeveloper\BetterDocs\Admin\ReportEmail;
28 38 use WPDeveloper\BetterDocs\FrontEnd\FrontEnd;
29 -use WPDeveloper\BetterDocs\Core\ShortcodeFactory;
39 +use WPDeveloper\BetterDocs\FrontEnd\PrintTemplate;
40 +use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
30 41 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
31 -use WPDeveloper\BetterDocs\Admin\Customizer\Customizer;
32 -use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
42 +use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
43 +use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
44 +use WPDeveloper\BetterDocs\Mcp\MCPManager;
45 +use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
46 +use WPDeveloper\BetterDocs\Utils\Database;
47 +use WPDeveloper\BetterDocs\Utils\Enqueue;
48 +use WPDeveloper\BetterDocs\Utils\Helper;
49 +use WPDeveloper\BetterDocs\Utils\Views;
33 50
34 51 final class Plugin {
35 52 private static $_instance = null;
36 53 /**
@@ -86,8 +103,13 @@
86 103 * @var TemplateTags
87 104 */
88 105 public $template_helper;
89 106 /**
107 + * Article Summary class
108 + * @var ArticleSummary
109 + */
110 + public $article_summary;
111 + /**
90 112 * Customizer class
91 113 * @var Customizer
92 114 */
93 115 public $customizer;
@@ -111,12 +133,32 @@
111 133 * @var Analytics
112 134 */
113 135 public $analytics;
114 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 + /**
115 157 * Plugin Version
116 158 * @var string
117 159 */
118 - public $version = '3.4.2';
160 + public $version = '4.9.4';
119 161
120 162 /**
121 163 * WriteWithAI Class
122 164 * @var string
@@ -121,39 +163,93 @@
121 163 * WriteWithAI Class
122 164 * @var string
123 165 */
124 166 public $ai_autowrtie;
167 + public $backgroundProccessor;
125 168
126 169 /**
170 + * ArticleQualityScore Class
171 + * @var ArticleQualityScore
172 + */
173 + public $article_quality_score;
174 +
175 + /**
127 176 * Plugin DB Version
128 177 * @var string
129 178 */
130 - public $db_version = '1.0.1';
179 + public $db_version = '1.0.3';
131 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 +
132 187 public function __construct() {
133 188 $this->define_constants();
134 189
135 190 do_action( 'betterdocs_init_before' );
191 + $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
192 +
136 193 $this->setup_container();
194 +
137 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 + /**
138 208 * Register activation and deactivation hooks
139 209 * and version updates check
140 210 */
141 211 $this->container->get( Install::class );
142 212
143 - $this->initialize();
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 );
144 225
145 - add_action( 'init', [ $this, 'init' ], 0 );
226 + add_action( 'init', array( $this, 'initialize' ), 0 );
146 227
147 228 /**
229 + * Initialize API
230 + */
231 + add_action( 'rest_api_init', array( $this, 'api_initialization' ) );
232 +
233 + /**
148 234 * For admin only
149 235 */
150 - add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
236 + add_action( 'admin_init', array( $this, 'admin_init' ), 0 );
151 237
152 238 /**
153 239 * For AJAX only
154 240 */
155 241 $this->ajax();
242 +
243 + /**
244 + * Style Handler For Parsing and Saving Styles as file.
245 + */
246 + ModulesStyleHandler::init();
247 +
248 + /**
249 + * Serve converted GIFs in docs as video.
250 + */
251 + \WPDeveloper\BetterDocs\Modules\GifVideo::init();
156 252 }
157 253
158 254 private function define_constants() {
159 255 $this->define( 'BETTERDOCS_VERSION', $this->version );
@@ -160,9 +256,11 @@
160 256 $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
161 257 $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
162 258 $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
163 259 $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
164 - $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/' );
165 263 $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
166 264 $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
167 265
168 266 /**
@@ -171,8 +269,10 @@
171 269 *
172 270 * WPML compatibility with Polylang
173 271 */
174 272 if ( Helper::is_plugin_active( 'polylang/polylang.php' ) ) {
273 + // Polylang's documented integration constant — must use the upstream-defined name.
274 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
175 275 define( 'PLL_WPML_COMPAT', false );
176 276 }
177 277 }
178 278
@@ -178,13 +278,15 @@
178 278
179 279 /**
180 280 * Define constant if not already set.
181 281 *
182 - * @param string $name Constant name.
282 + * @param string $name Constant name.
183 283 * @param string|bool $value Constant value.
184 284 */
185 285 private function define( $name, $value ) {
186 286 if ( ! defined( $name ) ) {
287 + // Caller passes plugin-prefixed names (BETTERDOCS_*); $name comes from a controlled internal allowlist.
288 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound
187 289 define( $name, $value );
188 290 }
189 291 }
190 292
@@ -196,59 +298,156 @@
196 298 $builder->addDefinitions( $config );
197 299 $this->container = $builder->build();
198 300 }
199 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 +
200 378 public function initialize() {
379 +
380 + /**
381 + * Setup localization.
382 + */
383 + $this->load_plugin_textdomain();
384 +
201 385 $this->container->get( Scripts::class );
202 386 $this->rewrite = $this->container->get( Rewrite::class );
203 387 $this->request = $this->container->get( Request::class );
204 388 $this->query = $this->container->get( Query::class );
205 389
390 + // Initialize background process
391 + $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
392 +
206 393 $this->rewrite->init();
207 394 $this->request->init();
208 395
209 - $this->assets = $this->container->get( Enqueue::class );
210 - $this->views = $this->container->get( Views::class );
211 - $this->helper = $this->container->get( Helper::class );
212 - $this->kbmigration = $this->container->get( KBMigration::class );
213 - $this->admin = $this->container->get( Admin::class );
214 - $this->database = $this->container->get( Database::class );
215 - $this->settings = $this->container->get( Settings::class );
216 - $this->analytics = $this->container->get( Analytics::class );
396 + $this->assets = $this->container->get( Enqueue::class );
397 + $this->views = $this->container->get( Views::class );
398 + $this->helper = $this->container->get( Helper::class );
399 + $this->kbmigration = $this->container->get( KBMigration::class );
400 + $this->admin = $this->container->get( Admin::class );
401 + $this->database = $this->container->get( Database::class );
402 + $this->settings = $this->container->get( Settings::class );
403 + $this->analytics = $this->container->get( Analytics::class );
217 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 +
218 420 $this->template_helper = $this->container->get( TemplateTags::class );
219 421 $this->customizer = $this->container->get( Customizer::class );
220 422 $this->editor = $this->container->get( Editor::class );
221 - $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
423 + $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
424 + $this->article_summary = $this->container->get( ArticleSummary::class );
222 425
223 - /**
224 - * Initialize all editors.
225 - * Elementor, Gutenberg/BlockEditor
226 - */
227 - add_action( 'init', [ $this->editor, 'init' ] );
426 + // Initialize unified metabox before individual features
427 + $this->container->get( UnifiedMetabox::class );
428 + $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
228 429
229 - /**
230 - * Initialize API
231 - */
232 - add_action( 'rest_api_init', [ $this, 'api_initialization' ] );
233 - }
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 );
234 433
235 - /**
236 - * Initialize the BetterDocs Plugin
237 - *
238 - * @return void
239 - * @since 2.5.0
240 - */
241 - public function init() {
434 + $this->container->get( Admin::class );
435 + $this->container->get( Roles::class );
436 +
242 437 /**
243 - * Setup localization.
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.
244 442 */
245 - $this->load_plugin_textdomain();
443 + $this->container->get( MCPManager::class );
246 444
247 - $this->container->get( Admin::class );
248 - $this->container->get( Roles::class );
249 445 $this->container->get( ReportEmail::class );
250 -
446 + // Usage-analytics collector. Registered here (runs on every request, incl.
447 + // WP-Cron) rather than in Admin so its `betterdocs_insights_data` filter
448 + // callback is present whenever the tracking payload is built.
449 + $this->container->get( \WPDeveloper\BetterDocs\Insights\Collector::class );
251 450 /**
252 451 * Initialize Shortcode
253 452 * Make sure you have listed out all shortcode in shortcode factory.
254 453 */
@@ -254,10 +453,24 @@
254 453 */
255 454 $this->container->get( ShortcodeFactory::class )->init();
256 455
257 456 $this->container->get( FrontEnd::class );
457 + $this->container->get( SearchExtender::class );
258 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 +
464 + /**
465 + * Single-product FAQ rendering (WooCommerce). The class itself bails when
466 + * WooCommerce is inactive or the feature is disabled.
467 + */
468 + $this->container->get( WooProductFAQ::class );
469 +
259 470 do_action( 'betterdocs_init' );
471 +
472 + $this->editor->init();
260 473 }
261 474
262 475 /**
263 476 * Load plugins textdomain `betterdocs` into actions.
@@ -268,13 +481,16 @@
268 481
269 482 /**
270 483 * Filter to adjust the BetterDocs locale to use for translations.
271 484 */
485 + // 'plugin_locale' is a WP-core filter; using its documented name is required.
486 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
272 487 $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
273 488
274 - unload_textdomain( $textdomain );
275 - load_textdomain( $textdomain, WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' );
276 - load_plugin_textdomain( $textdomain, false, plugin_basename( dirname( $plugin_file ) ) . '/languages' );
489 + if ( file_exists( WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' ) ) {
490 + unload_textdomain( $textdomain );
491 + load_textdomain( $textdomain, WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' );
492 + }
277 493 }
278 494
279 495 /**
280 496 * For AJAX Only
@@ -293,15 +509,16 @@
293 509 * @suppress PHP0441
294 510 * @since 2.5.0
295 511 */
296 512 public static function get_instance() {
297 - if ( static::$_instance == null ) {
298 - static::$_instance = new self();
513 + if ( null == self::$_instance ) {
514 + self::$_instance = new self();
299 515
300 516 do_action( 'betterdocs_loaded' );
517 + self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
301 518 }
302 519
303 - return static::$_instance;
520 + return self::$_instance;
304 521 }
305 522
306 523 /**
307 524 * Hooked with `admin_init` action.
@@ -328,14 +545,18 @@
328 545 // Delete the activation transient.
329 546 $this->database->delete_transient( 'betterdocs_maybe_redirect' );
330 547
331 548 if ( ! is_multisite() ) {
332 - $betterdocs_settings = get_option('betterdocs_settings');
333 - if ( $betterdocs_settings) {
334 - wp_safe_redirect( add_query_arg( [ 'page' => 'betterdocs-settings' ], admin_url( 'admin.php' ) ) );
549 + $betterdocs_settings = get_option( 'betterdocs_settings' );
550 + if ( $betterdocs_settings ) {
551 + wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-settings' ), admin_url( 'admin.php' ) ) );
335 552 } else {
336 - wp_safe_redirect( add_query_arg( [ 'page' => 'betterdocs-setup' ], admin_url( 'admin.php' ) ) );
553 + wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-setup' ), admin_url( 'admin.php' ) ) );
337 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;
338 559 }
339 560 }
340 561
341 562 /**
@@ -349,13 +570,161 @@
349 570 /**
350 571 * Is Pro Plugin Is Active?
351 572 * @return bool
352 573 */
574 + public function pro_file() {
575 + return WP_PLUGIN_DIR . '/betterdocs-pro/betterdocs-pro.php';
576 + }
577 +
353 578 public function is_pro_active() {
354 - return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
579 + if ( file_exists( $this->pro_file() ) ) {
580 + return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
581 + }
582 +
583 + return false;
355 584 }
356 585
586 + public function chatbot_file() {
587 + return WP_PLUGIN_DIR . '/betterdocs-ai-chatbot/betterdocs-ai-chatbot.php';
588 + }
589 +
590 + public function is_chatbot_active() {
591 + if ( file_exists( $this->chatbot_file() ) ) {
592 + return $this->helper->is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
593 + }
594 +
595 + return false;
596 + }
597 +
357 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 +
712 + public function pro_version() {
713 + if ( ! $this->is_pro_active() ) {
714 + return false;
715 + }
716 +
717 + if ( ! function_exists( 'get_plugin_data' ) ) {
718 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
719 + }
720 +
721 + $plugin_data = get_plugin_data( $this->pro_file() );
722 +
723 + return $plugin_data[ 'Version' ];
724 + }
725 +
726 + /**
358 727 * Get all the API initialized.
359 728 * @return void
360 729 */
361 730 public function api_initialization() {
@@ -362,9 +731,9 @@
362 731 $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
363 732
364 733 if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
365 734 foreach ( $_api_classes as $class ) {
366 - if ( $class == '.' || $class == '..' || strpos( $class, '.' ) === 0) {
735 + if ( '.' == $class || '..' == $class || strpos( $class, '.' ) === 0 ) {
367 736 continue;
368 737 }
369 738
370 739 $classname = basename( $class, '.php' );
@@ -375,6 +744,69 @@
375 744 $_api_class->register();
376 745 }
377 746 }
378 747 }
748 + }
749 +
750 + public function is_betterdocs_screen( $hook, $admin_check = true ): bool {
751 + /**
752 + * Filter the list of admin screen hook suffixes treated as BetterDocs
753 + * screens (controls whether the React admin bundle + styles load).
754 + * Pro/add-ons can register their own React pages here, e.g. the
755 + * Knowledge Base admin page.
756 + *
757 + * @param string[] $screens Full hook suffixes (e.g. betterdocs_page_betterdocs-foo).
758 + */
759 + $screens = apply_filters( 'betterdocs_admin_screens', array(
760 + 'toplevel_page_betterdocs-dashboard',
761 + 'toplevel_page_betterdocs-admin',
762 + 'admin_page_betterdocs-admin',
763 + 'betterdocs_page_betterdocs-admin',
764 + 'betterdocs_page_betterdocs-analytics',
765 + 'betterdocs_page_betterdocs-content-iq',
766 + 'betterdocs_page_betterdocs-settings',
767 + 'betterdocs_page_betterdocs-mcp',
768 + 'betterdocs_page_betterdocs-faq',
769 + 'betterdocs_page_betterdocs-glossaries',
770 + 'betterdocs_page_betterdocs-ai-chatbot',
771 + 'betterdocs_page_betterdocs-api-docs',
772 + 'betterdocs_page_betterdocs-doc-categories',
773 + 'betterdocs_page_betterdocs-doc-tags',
774 + ) );
775 +
776 + if ( $admin_check ) {
777 + if ( in_array( $hook, $screens ) ) {
778 + return true;
779 + }
780 +
781 + return false;
782 + }
783 +
784 + return false;
785 + }
786 +
787 + public function get_betterdocs_screen() {
788 + $registered_screens = array(
789 + 'toplevel_page_betterdocs-dashboard',
790 + 'admin_page_betterdocs-admin',
791 + 'betterdocs_page_betterdocs-admin',
792 + 'betterdocs_page_betterdocs-settings',
793 + 'betterdocs_page_betterdocs-mcp',
794 + 'betterdocs_page_betterdocs-analytics',
795 + 'betterdocs_page_betterdocs-faq',
796 + 'betterdocs_page_betterdocs-glossaries',
797 + 'betterdocs_page_betterdocs-ai-chatbot',
798 + 'betterdocs_page_betterdocs-api-docs',
799 + 'betterdocs_page_betterdocs-doc-categories',
800 + 'betterdocs_page_betterdocs-doc-tags',
801 + 'edit-docs'
802 + );
803 +
804 + $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
805 +
806 + if ( in_array( $current_screen_id, $registered_screens ) ) {
807 + return $current_screen_id;
808 + }
809 +
810 + return false;
379 811 }
380 812 }