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 +708 -376 4.4.0 → 4.9.4 View file →
@@ -2,9 +2,9 @@
2 2
3 3 namespace WPDeveloper\BetterDocs;
4 4
5 5 if ( ! defined( 'ABSPATH' ) ) {
6 - exit; // Exit if accessed directly
6 + exit; // Exit if accessed directly
7 7 }
8 8
9 9 use WPDeveloper\BetterDocs\Admin\Analytics;
10 10 use WPDeveloper\BetterDocs\Admin\Customizer\Customizer;
@@ -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,13 +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;
40 +use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
32 41 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
42 +use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
43 +use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
44 +use WPDeveloper\BetterDocs\Mcp\MCPManager;
33 45 use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
34 46 use WPDeveloper\BetterDocs\Utils\Database;
35 47 use WPDeveloper\BetterDocs\Utils\Enqueue;
36 48 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -36,445 +48,765 @@
36 48 use WPDeveloper\BetterDocs\Utils\Helper;
37 49 use WPDeveloper\BetterDocs\Utils\Views;
38 50
39 51 final class Plugin {
40 - private static $_instance = null;
41 - /**
42 - * Assets manager
43 - *
44 - * @var Enqueue
45 - */
46 - public $assets;
47 - /**
48 - * View manager
49 - *
50 - * @var Views
51 - */
52 - public $views;
53 - /**
54 - * Container Manager
55 - *
56 - * @var Container
57 - */
58 - public $container;
59 - /**
60 - * Editor Manager
61 - * @var Editor
62 - */
63 - public $editor;
64 - /**
65 - * Helper class
66 - * @var Helper
67 - */
68 - public $helper;
69 - /**
70 - * KBMigration class
71 - * @var KBMigration
72 - */
73 - public $kbmigration;
74 - /**
75 - * KBMigration class
76 - * @var Admin
77 - */
78 - public $admin;
79 - /**
80 - * Helper class
81 - * @var Database
82 - */
83 - public $database;
84 - /**
85 - * Helper class
86 - * @var Settings
87 - */
88 - public $settings;
89 - /**
90 - * Helper class
91 - * @var TemplateTags
92 - */
93 - public $template_helper;
94 - /**
95 - * Article Summary class
96 - * @var ArticleSummary
97 - */
98 - public $article_summary;
99 - /**
100 - * Customizer class
101 - * @var Customizer
102 - */
103 - public $customizer;
104 - /**
105 - * Query class
106 - * @var Query
107 - */
108 - public $query;
109 - /**
110 - * Rewrite Class
111 - * @var Rewrite
112 - */
113 - public $rewrite;
114 - /**
115 - * Request Class
116 - * @var Request
117 - */
118 - public $request;
119 - /**
120 - * Analytics Class
121 - * @var Analytics
122 - */
123 - public $analytics;
124 - /**
125 - * Plugin Version
126 - * @var string
127 - */
128 - public $version = '4.4.0';
52 + private static $_instance = null;
53 + /**
54 + * Assets manager
55 + *
56 + * @var Enqueue
57 + */
58 + public $assets;
59 + /**
60 + * View manager
61 + *
62 + * @var Views
63 + */
64 + public $views;
65 + /**
66 + * Container Manager
67 + *
68 + * @var Container
69 + */
70 + public $container;
71 + /**
72 + * Editor Manager
73 + * @var Editor
74 + */
75 + public $editor;
76 + /**
77 + * Helper class
78 + * @var Helper
79 + */
80 + public $helper;
81 + /**
82 + * KBMigration class
83 + * @var KBMigration
84 + */
85 + public $kbmigration;
86 + /**
87 + * KBMigration class
88 + * @var Admin
89 + */
90 + public $admin;
91 + /**
92 + * Helper class
93 + * @var Database
94 + */
95 + public $database;
96 + /**
97 + * Helper class
98 + * @var Settings
99 + */
100 + public $settings;
101 + /**
102 + * Helper class
103 + * @var TemplateTags
104 + */
105 + public $template_helper;
106 + /**
107 + * Article Summary class
108 + * @var ArticleSummary
109 + */
110 + public $article_summary;
111 + /**
112 + * Customizer class
113 + * @var Customizer
114 + */
115 + public $customizer;
116 + /**
117 + * Query class
118 + * @var Query
119 + */
120 + public $query;
121 + /**
122 + * Rewrite Class
123 + * @var Rewrite
124 + */
125 + public $rewrite;
126 + /**
127 + * Request Class
128 + * @var Request
129 + */
130 + public $request;
131 + /**
132 + * Analytics Class
133 + * @var Analytics
134 + */
135 + public $analytics;
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 + /**
157 + * Plugin Version
158 + * @var string
159 + */
160 + public $version = '4.9.4';
129 161
130 - /**
131 - * WriteWithAI Class
132 - * @var string
133 - */
134 - public $ai_autowrtie;
135 - public $backgroundProccessor;
162 + /**
163 + * WriteWithAI Class
164 + * @var string
165 + */
166 + public $ai_autowrtie;
167 + public $backgroundProccessor;
136 168
137 - /**
138 - * ArticleQualityScore Class
139 - * @var ArticleQualityScore
140 - */
141 - public $article_quality_score;
169 + /**
170 + * ArticleQualityScore Class
171 + * @var ArticleQualityScore
172 + */
173 + public $article_quality_score;
142 174
143 - /**
144 - * Plugin DB Version
145 - * @var string
146 - */
147 - public $db_version = '1.0.1';
175 + /**
176 + * Plugin DB Version
177 + * @var string
178 + */
179 + public $db_version = '1.0.3';
148 180
149 - public function __construct() {
150 - $this->define_constants();
181 + /**
182 + * Listeners attached to each load-time hook when it fired, keyed by hook.
183 + * @var array
184 + */
185 + private $fired_hook_callbacks = [];
151 186
152 - do_action( 'betterdocs_init_before' );
187 + public function __construct() {
188 + $this->define_constants();
153 189
154 - $this->setup_container();
155 - /**
156 - * Register activation and deactivation hooks
157 - * and version updates check
158 - */
159 - $this->container->get( Install::class );
190 + do_action( 'betterdocs_init_before' );
191 + $this->fired_hook_callbacks['betterdocs_init_before'] = $this->get_hook_callbacks( 'betterdocs_init_before' );
160 192
161 - add_action( 'init', [ $this, 'initialize' ], 0 );
193 + $this->setup_container();
162 194
163 - /**
164 - * Initialize API
165 - */
166 - add_action( 'rest_api_init', [ $this, 'api_initialization' ] );
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 + /**
208 + * Register activation and deactivation hooks
209 + * and version updates check
210 + */
211 + $this->container->get( Install::class );
167 212
168 - /**
169 - * For admin only
170 - */
171 - add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
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 );
172 225
173 - /**
174 - * For AJAX only
175 - */
176 - $this->ajax();
226 + add_action( 'init', array( $this, 'initialize' ), 0 );
177 227
178 - /**
179 - * Style Handler For Parsing and Saving Styles as file.
180 - */
181 - ModulesStyleHandler::init();
182 - }
228 + /**
229 + * Initialize API
230 + */
231 + add_action( 'rest_api_init', array( $this, 'api_initialization' ) );
183 232
184 - private function define_constants() {
185 - $this->define( 'BETTERDOCS_VERSION', $this->version );
186 - $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
187 - $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
188 - $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
189 - $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
190 - $this->define( 'BETTERDOCS_BLOCKS_DIRECTORY', BETTERDOCS_ABSPATH . 'assets/blocks/' );
191 - $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
192 - $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
233 + /**
234 + * For admin only
235 + */
236 + add_action( 'admin_init', array( $this, 'admin_init' ), 0 );
193 237
194 - /**
195 - * Third Party Constants
196 - * @since 2.5.0
197 - *
198 - * WPML compatibility with Polylang
199 - */
200 - if ( Helper::is_plugin_active( 'polylang/polylang.php' ) ) {
201 - define( 'PLL_WPML_COMPAT', false );
202 - }
203 - }
238 + /**
239 + * For AJAX only
240 + */
241 + $this->ajax();
204 242
205 - /**
206 - * Define constant if not already set.
207 - *
208 - * @param string $name Constant name.
209 - * @param string|bool $value Constant value.
210 - */
211 - private function define( $name, $value ) {
212 - if ( ! defined( $name ) ) {
213 - define( $name, $value );
214 - }
215 - }
243 + /**
244 + * Style Handler For Parsing and Saving Styles as file.
245 + */
246 + ModulesStyleHandler::init();
216 247
217 - public function setup_container() {
218 - $config_array = require_once BETTERDOCS_ABSPATH . 'includes/config.php';
219 - $config = apply_filters( 'betterdocs_container_config', $config_array );
220 - $builder = new ContainerBuilder();
248 + /**
249 + * Serve converted GIFs in docs as video.
250 + */
251 + \WPDeveloper\BetterDocs\Modules\GifVideo::init();
252 + }
221 253
222 - $builder->addDefinitions( $config );
223 - $this->container = $builder->build();
224 - }
254 + private function define_constants() {
255 + $this->define( 'BETTERDOCS_VERSION', $this->version );
256 + $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
257 + $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
258 + $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
259 + $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
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/' );
263 + $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
264 + $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
225 265
226 - public function initialize() {
266 + /**
267 + * Third Party Constants
268 + * @since 2.5.0
269 + *
270 + * WPML compatibility with Polylang
271 + */
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
275 + define( 'PLL_WPML_COMPAT', false );
276 + }
277 + }
227 278
228 - /**
229 - * Setup localization.
230 - */
231 - $this->load_plugin_textdomain();
279 + /**
280 + * Define constant if not already set.
281 + *
282 + * @param string $name Constant name.
283 + * @param string|bool $value Constant value.
284 + */
285 + private function define( $name, $value ) {
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
289 + define( $name, $value );
290 + }
291 + }
232 292
233 - $this->container->get( Scripts::class );
234 - $this->rewrite = $this->container->get( Rewrite::class );
235 - $this->request = $this->container->get( Request::class );
236 - $this->query = $this->container->get( Query::class );
293 + public function setup_container() {
294 + $config_array = require_once BETTERDOCS_ABSPATH . 'includes/config.php';
295 + $config = apply_filters( 'betterdocs_container_config', $config_array );
296 + $builder = new ContainerBuilder();
237 297
238 - // Initialize background process
239 - $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
298 + $builder->addDefinitions( $config );
299 + $this->container = $builder->build();
300 + }
240 301
241 - $this->rewrite->init();
242 - $this->request->init();
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' );
243 318
244 - $this->assets = $this->container->get( Enqueue::class );
245 - $this->views = $this->container->get( Views::class );
246 - $this->helper = $this->container->get( Helper::class );
247 - $this->kbmigration = $this->container->get( KBMigration::class );
248 - $this->admin = $this->container->get( Admin::class );
249 - $this->database = $this->container->get( Database::class );
250 - $this->settings = $this->container->get( Settings::class );
251 - $this->analytics = $this->container->get( Analytics::class );
319 + if ( $this->run_late_listeners( 'betterdocs_init_before' ) ) {
320 + $late_filters = array_diff_key( $this->get_hook_callbacks( 'betterdocs_container_config' ), $config_filters );
252 321
253 - $this->template_helper = $this->container->get( TemplateTags::class );
254 - $this->customizer = $this->container->get( Customizer::class );
255 - $this->editor = $this->container->get( Editor::class );
256 - $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
257 - $this->article_summary = $this->container->get( ArticleSummary::class );
322 + $config = [];
323 + foreach ( $late_filters as $filter ) {
324 + $config = call_user_func( $filter['function'], $config );
325 + }
258 326
259 - // Initialize unified metabox before individual features
260 - $this->container->get( UnifiedMetabox::class );
261 - $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
327 + if ( is_array( $config ) ) {
328 + foreach ( $config as $id => $definition ) {
329 + $this->container->set( $id, $definition );
330 + }
331 + }
332 + }
262 333
263 - $this->container->get( Admin::class );
264 - $this->container->get( Roles::class );
265 - $this->container->get( ReportEmail::class );
266 - /**
267 - * Initialize Shortcode
268 - * Make sure you have listed out all shortcode in shortcode factory.
269 - */
270 - $this->container->get( ShortcodeFactory::class )->init();
334 + $this->run_late_listeners( 'betterdocs_loaded' );
335 + }
271 336
272 - $this->container->get( FrontEnd::class );
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 );
273 346
274 - do_action( 'betterdocs_init' );
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 + }
275 351
276 - $this->editor->init();
277 - }
352 + return ! empty( $listeners );
353 + }
278 354
279 - /**
280 - * Load plugins textdomain `betterdocs` into actions.
281 - * @return void
282 - */
283 - public function load_plugin_textdomain( $textdomain = 'betterdocs', $plugin_file = BETTERDOCS_PLUGIN_FILE ) {
284 - $locale = determine_locale();
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;
285 363
286 - /**
287 - * Filter to adjust the BetterDocs locale to use for translations.
288 - */
289 - $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
364 + $callbacks = [];
365 + if ( empty( $wp_filter[ $hook ] ) || ! $wp_filter[ $hook ] instanceof \WP_Hook ) {
366 + return $callbacks;
367 + }
290 368
291 - if ( file_exists( WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' ) ) {
292 - unload_textdomain( $textdomain );
293 - load_textdomain( $textdomain, WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' );
294 - }
369 + foreach ( $wp_filter[ $hook ]->callbacks as $priority => $items ) {
370 + foreach ( $items as $id => $callback ) {
371 + $callbacks[ $priority . '|' . $id ] = $callback;
372 + }
373 + }
295 374
296 - load_plugin_textdomain( $textdomain, false, plugin_basename( dirname( $plugin_file ) ) . '/languages' );
297 - }
375 + return $callbacks;
376 + }
298 377
299 - /**
300 - * For AJAX Only
301 - * @return void
302 - */
303 - public function ajax() {
304 - }
378 + public function initialize() {
305 379
306 - /**
307 - * Create a plugin instance.
308 - *
309 - * @param mixed ...$args
310 - *
311 - * @return static
312 - *
313 - * @suppress PHP0441
314 - * @since 2.5.0
315 - */
316 - public static function get_instance() {
317 - if ( self::$_instance == null ) {
318 - self::$_instance = new self();
380 + /**
381 + * Setup localization.
382 + */
383 + $this->load_plugin_textdomain();
319 384
320 - do_action( 'betterdocs_loaded' );
321 - }
385 + $this->container->get( Scripts::class );
386 + $this->rewrite = $this->container->get( Rewrite::class );
387 + $this->request = $this->container->get( Request::class );
388 + $this->query = $this->container->get( Query::class );
322 389
323 - return self::$_instance;
324 - }
390 + // Initialize background process
391 + $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
325 392
326 - /**
327 - * Hooked with `admin_init` action.
328 - * @return void
329 - */
330 - public function admin_init() {
331 - /**
332 - * Maybe Redirect
333 - * for setup related settings.
334 - */
335 - $this->maybe_redirect();
336 - }
393 + $this->rewrite->init();
394 + $this->request->init();
337 395
338 - /**
339 - * Summary of maybe_redirect
340 - * @return void
341 - */
342 - public function maybe_redirect() {
343 - // Bail if no activation transient is set.
344 - if ( ! $this->database->get_transient( 'betterdocs_maybe_redirect' ) ) {
345 - return;
346 - }
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 );
347 404
348 - // Delete the activation transient.
349 - $this->database->delete_transient( 'betterdocs_maybe_redirect' );
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 );
350 414
351 - if ( ! is_multisite() ) {
352 - $betterdocs_settings = get_option( 'betterdocs_settings' );
353 - if ( $betterdocs_settings ) {
354 - wp_safe_redirect( add_query_arg( [ 'page' => 'betterdocs-settings' ], admin_url( 'admin.php' ) ) );
355 - } else {
356 - wp_safe_redirect( add_query_arg( [ 'page' => 'betterdocs-setup' ], admin_url( 'admin.php' ) ) );
357 - }
358 - }
359 - }
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 );
360 419
361 - /**
362 - * Is Pro Plugin Is Installed?
363 - * @return bool
364 - */
365 - public function is_pro_installed() {
366 - return $this->helper->get_plugins( 'betterdocs-pro/betterdocs-pro.php' );
367 - }
420 + $this->template_helper = $this->container->get( TemplateTags::class );
421 + $this->customizer = $this->container->get( Customizer::class );
422 + $this->editor = $this->container->get( Editor::class );
423 + $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
424 + $this->article_summary = $this->container->get( ArticleSummary::class );
368 425
369 - /**
370 - * Is Pro Plugin Is Active?
371 - * @return bool
372 - */
373 - public function pro_file() {
374 - return WP_PLUGIN_DIR . '/betterdocs-pro/betterdocs-pro.php';
375 - }
426 + // Initialize unified metabox before individual features
427 + $this->container->get( UnifiedMetabox::class );
428 + $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
376 429
377 - public function is_pro_active() {
378 - if ( file_exists( $this->pro_file() ) ) {
379 - return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
380 - }
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 );
381 433
382 - return false;
383 - }
434 + $this->container->get( Admin::class );
435 + $this->container->get( Roles::class );
384 436
385 - public function chatbot_file() {
386 - return WP_PLUGIN_DIR . '/betterdocs-ai-chatbot/betterdocs-ai-chatbot.php';
387 - }
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 );
388 444
389 - public function is_chatbot_active() {
390 - if ( file_exists( $this->chatbot_file() ) ) {
391 - return $this->helper->is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
392 - }
445 + $this->container->get( ReportEmail::class );
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 );
450 + /**
451 + * Initialize Shortcode
452 + * Make sure you have listed out all shortcode in shortcode factory.
453 + */
454 + $this->container->get( ShortcodeFactory::class )->init();
393 455
394 - return false;
395 - }
456 + $this->container->get( FrontEnd::class );
457 + $this->container->get( SearchExtender::class );
396 458
397 - public function pro_version() {
398 - if ( ! $this->is_pro_active() ) {
399 - return false;
400 - }
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 );
401 463
402 - if ( ! function_exists( 'get_plugin_data' ) ) {
403 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
404 - }
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 );
405 469
406 - $plugin_data = get_plugin_data( $this->pro_file() );
470 + do_action( 'betterdocs_init' );
407 471
408 - return $plugin_data['Version'];
409 - }
472 + $this->editor->init();
473 + }
410 474
411 - /**
412 - * Get all the API initialized.
413 - * @return void
414 - */
415 - public function api_initialization() {
416 - $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
475 + /**
476 + * Load plugins textdomain `betterdocs` into actions.
477 + * @return void
478 + */
479 + public function load_plugin_textdomain( $textdomain = 'betterdocs', $plugin_file = BETTERDOCS_PLUGIN_FILE ) {
480 + $locale = determine_locale();
417 481
418 - if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
419 - foreach ( $_api_classes as $class ) {
420 - if ( $class == '.' || $class == '..' || strpos( $class, '.' ) === 0 ) {
421 - continue;
422 - }
482 + /**
483 + * Filter to adjust the BetterDocs locale to use for translations.
484 + */
485 + // 'plugin_locale' is a WP-core filter; using its documented name is required.
486 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
487 + $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
423 488
424 - $classname = basename( $class, '.php' );
425 - $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
426 - $_api_class = $this->container->get( $classname );
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 + }
493 + }
427 494
428 - if ( $_api_class instanceof BaseAPI ) {
429 - $_api_class->register();
430 - }
431 - }
432 - }
433 - }
495 + /**
496 + * For AJAX Only
497 + * @return void
498 + */
499 + public function ajax() {
500 + }
434 501
435 - public function is_betterdocs_screen( $hook, $admin_check = true ): bool {
436 - $screens = [
437 - 'toplevel_page_betterdocs-dashboard',
438 - 'toplevel_page_betterdocs-admin',
439 - 'admin_page_betterdocs-admin',
440 - 'betterdocs_page_betterdocs-admin',
441 - 'betterdocs_page_betterdocs-analytics',
442 - 'betterdocs_page_betterdocs-settings',
443 - 'betterdocs_page_betterdocs-faq',
444 - 'betterdocs_page_betterdocs-glossaries',
502 + /**
503 + * Create a plugin instance.
504 + *
505 + * @param mixed ...$args
506 + *
507 + * @return static
508 + *
509 + * @suppress PHP0441
510 + * @since 2.5.0
511 + */
512 + public static function get_instance() {
513 + if ( null == self::$_instance ) {
514 + self::$_instance = new self();
515 +
516 + do_action( 'betterdocs_loaded' );
517 + self::$_instance->fired_hook_callbacks['betterdocs_loaded'] = self::$_instance->get_hook_callbacks( 'betterdocs_loaded' );
518 + }
519 +
520 + return self::$_instance;
521 + }
522 +
523 + /**
524 + * Hooked with `admin_init` action.
525 + * @return void
526 + */
527 + public function admin_init() {
528 + /**
529 + * Maybe Redirect
530 + * for setup related settings.
531 + */
532 + $this->maybe_redirect();
533 + }
534 +
535 + /**
536 + * Summary of maybe_redirect
537 + * @return void
538 + */
539 + public function maybe_redirect() {
540 + // Bail if no activation transient is set.
541 + if ( ! $this->database->get_transient( 'betterdocs_maybe_redirect' ) ) {
542 + return;
543 + }
544 +
545 + // Delete the activation transient.
546 + $this->database->delete_transient( 'betterdocs_maybe_redirect' );
547 +
548 + if ( ! is_multisite() ) {
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' ) ) );
552 + } else {
553 + wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-setup' ), admin_url( 'admin.php' ) ) );
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;
559 + }
560 + }
561 +
562 + /**
563 + * Is Pro Plugin Is Installed?
564 + * @return bool
565 + */
566 + public function is_pro_installed() {
567 + return $this->helper->get_plugins( 'betterdocs-pro/betterdocs-pro.php' );
568 + }
569 +
570 + /**
571 + * Is Pro Plugin Is Active?
572 + * @return bool
573 + */
574 + public function pro_file() {
575 + return WP_PLUGIN_DIR . '/betterdocs-pro/betterdocs-pro.php';
576 + }
577 +
578 + public function is_pro_active() {
579 + if ( file_exists( $this->pro_file() ) ) {
580 + return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
581 + }
582 +
583 + return false;
584 + }
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 +
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 + /**
727 + * Get all the API initialized.
728 + * @return void
729 + */
730 + public function api_initialization() {
731 + $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
732 +
733 + if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
734 + foreach ( $_api_classes as $class ) {
735 + if ( '.' == $class || '..' == $class || strpos( $class, '.' ) === 0 ) {
736 + continue;
737 + }
738 +
739 + $classname = basename( $class, '.php' );
740 + $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
741 + $_api_class = $this->container->get( $classname );
742 +
743 + if ( $_api_class instanceof BaseAPI ) {
744 + $_api_class->register();
745 + }
746 + }
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',
445 770 'betterdocs_page_betterdocs-ai-chatbot',
446 - ];
771 + 'betterdocs_page_betterdocs-api-docs',
772 + 'betterdocs_page_betterdocs-doc-categories',
773 + 'betterdocs_page_betterdocs-doc-tags',
774 + ) );
447 775
448 - if ( $admin_check ) {
449 - if ( in_array( $hook, $screens ) ) {
450 - return true;
451 - }
776 + if ( $admin_check ) {
777 + if ( in_array( $hook, $screens ) ) {
778 + return true;
779 + }
452 780
453 - return false;
454 - }
781 + return false;
782 + }
455 783
456 - return false;
457 - }
784 + return false;
785 + }
458 786
459 - public function get_betterdocs_screen() {
460 - $registered_screens = [
461 - 'toplevel_page_betterdocs-dashboard',
462 - 'admin_page_betterdocs-admin',
463 - 'betterdocs_page_betterdocs-admin',
464 - 'betterdocs_page_betterdocs-settings',
465 - 'betterdocs_page_betterdocs-analytics',
466 - 'betterdocs_page_betterdocs-faq',
467 - 'betterdocs_page_betterdocs-glossaries',
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',
468 797 'betterdocs_page_betterdocs-ai-chatbot',
469 - 'edit-docs',
470 - ];
798 + 'betterdocs_page_betterdocs-api-docs',
799 + 'betterdocs_page_betterdocs-doc-categories',
800 + 'betterdocs_page_betterdocs-doc-tags',
801 + 'edit-docs'
802 + );
471 803
472 - $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
804 + $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
473 805
474 - if ( in_array( $current_screen_id, $registered_screens ) ) {
475 - return $current_screen_id;
476 - }
806 + if ( in_array( $current_screen_id, $registered_screens ) ) {
807 + return $current_screen_id;
808 + }
477 809
478 - return false;
479 - }
810 + return false;
811 + }
480 812 }