PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
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 3.5.0 3.5.1 All 200 releases
← All changes | includes/Plugin.php +539 -376 4.4.04.9.2 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,10 @@
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\AnalyticsTracker;
15 +use WPDeveloper\BetterDocs\Core\AnalyticsRetention;
14 16 use WPDeveloper\BetterDocs\Core\BaseAPI;
15 17 use WPDeveloper\BetterDocs\Core\Install;
16 18 use WPDeveloper\BetterDocs\Core\KBMigration;
17 19 use WPDeveloper\BetterDocs\Core\Query;
@@ -24,13 +26,19 @@
24 26 use WPDeveloper\BetterDocs\Core\WriteWithAI;
25 27 use WPDeveloper\BetterDocs\Core\ArticleSummary;
26 28 use WPDeveloper\BetterDocs\Core\ArticleQualityScore;
27 29 use WPDeveloper\BetterDocs\Core\UnifiedMetabox;
30 +use WPDeveloper\BetterDocs\Core\DocsAISuite;
28 31 use WPDeveloper\BetterDocs\Dependencies\DI\Container;
29 32 use WPDeveloper\BetterDocs\Dependencies\DI\ContainerBuilder;
30 33 use WPDeveloper\BetterDocs\Editors\Editor;
31 34 use WPDeveloper\BetterDocs\FrontEnd\FrontEnd;
35 +use WPDeveloper\BetterDocs\FrontEnd\PrintTemplate;
36 +use WPDeveloper\BetterDocs\FrontEnd\SearchExtender;
32 37 use WPDeveloper\BetterDocs\FrontEnd\TemplateTags;
38 +use WPDeveloper\BetterDocs\FrontEnd\WooProductFAQ;
39 +use WPDeveloper\BetterDocs\Abilities\AbilitiesRegistrar;
40 +use WPDeveloper\BetterDocs\Mcp\MCPManager;
33 41 use WPDeveloper\BetterDocs\Modules\StyleHandler as ModulesStyleHandler;
34 42 use WPDeveloper\BetterDocs\Utils\Database;
35 43 use WPDeveloper\BetterDocs\Utils\Enqueue;
36 44 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -36,445 +44,600 @@
36 44 use WPDeveloper\BetterDocs\Utils\Helper;
37 45 use WPDeveloper\BetterDocs\Utils\Views;
38 46
39 47 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';
48 + private static $_instance = null;
49 + /**
50 + * Assets manager
51 + *
52 + * @var Enqueue
53 + */
54 + public $assets;
55 + /**
56 + * View manager
57 + *
58 + * @var Views
59 + */
60 + public $views;
61 + /**
62 + * Container Manager
63 + *
64 + * @var Container
65 + */
66 + public $container;
67 + /**
68 + * Editor Manager
69 + * @var Editor
70 + */
71 + public $editor;
72 + /**
73 + * Helper class
74 + * @var Helper
75 + */
76 + public $helper;
77 + /**
78 + * KBMigration class
79 + * @var KBMigration
80 + */
81 + public $kbmigration;
82 + /**
83 + * KBMigration class
84 + * @var Admin
85 + */
86 + public $admin;
87 + /**
88 + * Helper class
89 + * @var Database
90 + */
91 + public $database;
92 + /**
93 + * Helper class
94 + * @var Settings
95 + */
96 + public $settings;
97 + /**
98 + * Helper class
99 + * @var TemplateTags
100 + */
101 + public $template_helper;
102 + /**
103 + * Article Summary class
104 + * @var ArticleSummary
105 + */
106 + public $article_summary;
107 + /**
108 + * Customizer class
109 + * @var Customizer
110 + */
111 + public $customizer;
112 + /**
113 + * Query class
114 + * @var Query
115 + */
116 + public $query;
117 + /**
118 + * Rewrite Class
119 + * @var Rewrite
120 + */
121 + public $rewrite;
122 + /**
123 + * Request Class
124 + * @var Request
125 + */
126 + public $request;
127 + /**
128 + * Analytics Class
129 + * @var Analytics
130 + */
131 + public $analytics;
132 + /**
133 + * Plugin Version
134 + * @var string
135 + */
136 + public $version = '4.9.2';
129 137
130 - /**
131 - * WriteWithAI Class
132 - * @var string
133 - */
134 - public $ai_autowrtie;
135 - public $backgroundProccessor;
138 + /**
139 + * WriteWithAI Class
140 + * @var string
141 + */
142 + public $ai_autowrtie;
143 + public $backgroundProccessor;
136 144
137 - /**
138 - * ArticleQualityScore Class
139 - * @var ArticleQualityScore
140 - */
141 - public $article_quality_score;
145 + /**
146 + * ArticleQualityScore Class
147 + * @var ArticleQualityScore
148 + */
149 + public $article_quality_score;
142 150
143 - /**
144 - * Plugin DB Version
145 - * @var string
146 - */
147 - public $db_version = '1.0.1';
151 + /**
152 + * Plugin DB Version
153 + * @var string
154 + */
155 + public $db_version = '1.0.3';
148 156
149 - public function __construct() {
150 - $this->define_constants();
157 + public function __construct() {
158 + $this->define_constants();
151 159
152 - do_action( 'betterdocs_init_before' );
160 + do_action( 'betterdocs_init_before' );
153 161
154 - $this->setup_container();
155 - /**
156 - * Register activation and deactivation hooks
157 - * and version updates check
158 - */
159 - $this->container->get( Install::class );
162 + $this->setup_container();
163 + /**
164 + * Register activation and deactivation hooks
165 + * and version updates check
166 + */
167 + $this->container->get( Install::class );
160 168
161 - add_action( 'init', [ $this, 'initialize' ], 0 );
169 + /**
170 + * Abilities API registrar.
171 + *
172 + * Resolved here, at plugin load, and not from initialize(): both the
173 + * bundled Abilities API and WordPress core's build their registry as a
174 + * lazy singleton and fire `wp_abilities_api_init` from it, at or after
175 + * `init`, the first time anything reads the registry. Our listeners
176 + * therefore have to be attached before that first read, whenever it
177 + * happens — and a container resolve on `init` would already be too late
178 + * for a plugin that reads the registry earlier in the same hook.
179 + */
180 + $this->container->get( AbilitiesRegistrar::class );
162 181
163 - /**
164 - * Initialize API
165 - */
166 - add_action( 'rest_api_init', [ $this, 'api_initialization' ] );
182 + add_action( 'init', array( $this, 'initialize' ), 0 );
167 183
168 - /**
169 - * For admin only
170 - */
171 - add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
184 + /**
185 + * Initialize API
186 + */
187 + add_action( 'rest_api_init', array( $this, 'api_initialization' ) );
172 188
173 - /**
174 - * For AJAX only
175 - */
176 - $this->ajax();
189 + /**
190 + * For admin only
191 + */
192 + add_action( 'admin_init', array( $this, 'admin_init' ), 0 );
177 193
178 - /**
179 - * Style Handler For Parsing and Saving Styles as file.
180 - */
181 - ModulesStyleHandler::init();
182 - }
194 + /**
195 + * For AJAX only
196 + */
197 + $this->ajax();
183 198
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' );
199 + /**
200 + * Style Handler For Parsing and Saving Styles as file.
201 + */
202 + ModulesStyleHandler::init();
203 + }
193 204
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 - }
205 + private function define_constants() {
206 + $this->define( 'BETTERDOCS_VERSION', $this->version );
207 + $this->define( 'BETTERDOCS_DB_VERSION', $this->db_version );
208 + $this->define( 'BETTERDOCS_ABSPATH', dirname( BETTERDOCS_PLUGIN_FILE ) . '/' );
209 + $this->define( 'BETTERDOCS_ABSURL', plugin_dir_url( BETTERDOCS_PLUGIN_FILE ) );
210 + $this->define( 'BETTERDOCS_PLUGIN_BASENAME', plugin_basename( BETTERDOCS_PLUGIN_FILE ) );
211 + // Compiled block metadata lives under assets/build/ since the Node 24 asset
212 + // restructure; register_block_type() fails silently if this path is wrong.
213 + $this->define( 'BETTERDOCS_BLOCKS_DIRECTORY', BETTERDOCS_ABSPATH . 'assets/build/blocks/' );
214 + $this->define( 'BETTERDOCS_ROOT_DIR_PATH', plugin_dir_path( BETTERDOCS_PLUGIN_FILE ) );
215 + $this->define( 'BETTERDOCS_FSE_TEMPLATES_PATH', BETTERDOCS_ROOT_DIR_PATH . 'views/templates/fse' );
204 216
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 - }
217 + /**
218 + * Third Party Constants
219 + * @since 2.5.0
220 + *
221 + * WPML compatibility with Polylang
222 + */
223 + if ( Helper::is_plugin_active( 'polylang/polylang.php' ) ) {
224 + // Polylang's documented integration constant — must use the upstream-defined name.
225 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
226 + define( 'PLL_WPML_COMPAT', false );
227 + }
228 + }
216 229
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();
230 + /**
231 + * Define constant if not already set.
232 + *
233 + * @param string $name Constant name.
234 + * @param string|bool $value Constant value.
235 + */
236 + private function define( $name, $value ) {
237 + if ( ! defined( $name ) ) {
238 + // Caller passes plugin-prefixed names (BETTERDOCS_*); $name comes from a controlled internal allowlist.
239 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound
240 + define( $name, $value );
241 + }
242 + }
221 243
222 - $builder->addDefinitions( $config );
223 - $this->container = $builder->build();
224 - }
244 + public function setup_container() {
245 + $config_array = require_once BETTERDOCS_ABSPATH . 'includes/config.php';
246 + $config = apply_filters( 'betterdocs_container_config', $config_array );
247 + $builder = new ContainerBuilder();
225 248
226 - public function initialize() {
249 + $builder->addDefinitions( $config );
250 + $this->container = $builder->build();
251 + }
227 252
228 - /**
229 - * Setup localization.
230 - */
231 - $this->load_plugin_textdomain();
253 + public function initialize() {
232 254
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 );
255 + /**
256 + * Setup localization.
257 + */
258 + $this->load_plugin_textdomain();
237 259
238 - // Initialize background process
239 - $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
260 + $this->container->get( Scripts::class );
261 + $this->rewrite = $this->container->get( Rewrite::class );
262 + $this->request = $this->container->get( Request::class );
263 + $this->query = $this->container->get( Query::class );
240 264
241 - $this->rewrite->init();
242 - $this->request->init();
265 + // Initialize background process
266 + $this->backgroundProccessor = $this->container->get( HelpScoutMigration::class );
243 267
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 );
268 + $this->rewrite->init();
269 + $this->request->init();
252 270
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 );
271 + $this->assets = $this->container->get( Enqueue::class );
272 + $this->views = $this->container->get( Views::class );
273 + $this->helper = $this->container->get( Helper::class );
274 + $this->kbmigration = $this->container->get( KBMigration::class );
275 + $this->admin = $this->container->get( Admin::class );
276 + $this->database = $this->container->get( Database::class );
277 + $this->settings = $this->container->get( Settings::class );
278 + $this->analytics = $this->container->get( Analytics::class );
279 + // Free analytics collectors: the frontend view/scroll tracker and the
280 + // daily retention purge. Each self-registers its hooks on construct.
281 + $this->container->get( AnalyticsTracker::class );
282 + $this->container->get( AnalyticsRetention::class );
258 283
259 - // Initialize unified metabox before individual features
260 - $this->container->get( UnifiedMetabox::class );
261 - $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
284 + $this->template_helper = $this->container->get( TemplateTags::class );
285 + $this->customizer = $this->container->get( Customizer::class );
286 + $this->editor = $this->container->get( Editor::class );
287 + $this->ai_autowrtie = $this->container->get( WriteWithAI::class );
288 + $this->article_summary = $this->container->get( ArticleSummary::class );
262 289
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();
290 + // Initialize unified metabox before individual features
291 + $this->container->get( UnifiedMetabox::class );
292 + $this->article_quality_score = $this->container->get( ArticleQualityScore::class );
271 293
272 - $this->container->get( FrontEnd::class );
294 + // Editor "Suggest Categories / Tags / Glossaries" AI actions on the docs
295 + // post type (gated by enable_docs_ai_suite + an OpenAI key inside the class).
296 + $this->container->get( DocsAISuite::class );
273 297
274 - do_action( 'betterdocs_init' );
298 + $this->container->get( Admin::class );
299 + $this->container->get( Roles::class );
275 300
276 - $this->editor->init();
277 - }
301 + /**
302 + * MCP transport: rewrite rules, the pretty endpoint, OAuth discovery
303 + * and every REST route. Resolved here rather than at plugin load
304 + * (where the abilities registrar has to be) because its earliest hook
305 + * is `init`, which is what this method already runs on.
306 + */
307 + $this->container->get( MCPManager::class );
278 308
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();
309 + $this->container->get( ReportEmail::class );
310 + // Usage-analytics collector. Registered here (runs on every request, incl.
311 + // WP-Cron) rather than in Admin so its `betterdocs_insights_data` filter
312 + // callback is present whenever the tracking payload is built.
313 + $this->container->get( \WPDeveloper\BetterDocs\Insights\Collector::class );
314 + /**
315 + * Initialize Shortcode
316 + * Make sure you have listed out all shortcode in shortcode factory.
317 + */
318 + $this->container->get( ShortcodeFactory::class )->init();
285 319
286 - /**
287 - * Filter to adjust the BetterDocs locale to use for translations.
288 - */
289 - $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
320 + $this->container->get( FrontEnd::class );
321 + $this->container->get( SearchExtender::class );
290 322
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 - }
323 + // Running logo header / page footer for the browser's own Print command.
324 + // Registered unconditionally: it covers every front-end page, including
325 + // the ones that carry no BetterDocs print button.
326 + $this->container->get( PrintTemplate::class );
295 327
296 - load_plugin_textdomain( $textdomain, false, plugin_basename( dirname( $plugin_file ) ) . '/languages' );
297 - }
328 + /**
329 + * Single-product FAQ rendering (WooCommerce). The class itself bails when
330 + * WooCommerce is inactive or the feature is disabled.
331 + */
332 + $this->container->get( WooProductFAQ::class );
298 333
299 - /**
300 - * For AJAX Only
301 - * @return void
302 - */
303 - public function ajax() {
304 - }
334 + do_action( 'betterdocs_init' );
305 335
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();
336 + $this->editor->init();
337 + }
319 338
320 - do_action( 'betterdocs_loaded' );
321 - }
339 + /**
340 + * Load plugins textdomain `betterdocs` into actions.
341 + * @return void
342 + */
343 + public function load_plugin_textdomain( $textdomain = 'betterdocs', $plugin_file = BETTERDOCS_PLUGIN_FILE ) {
344 + $locale = determine_locale();
322 345
323 - return self::$_instance;
324 - }
346 + /**
347 + * Filter to adjust the BetterDocs locale to use for translations.
348 + */
349 + // 'plugin_locale' is a WP-core filter; using its documented name is required.
350 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
351 + $locale = apply_filters( 'plugin_locale', $locale, $textdomain );
325 352
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 - }
353 + if ( file_exists( WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' ) ) {
354 + unload_textdomain( $textdomain );
355 + load_textdomain( $textdomain, WP_LANG_DIR . "/$textdomain-" . $locale . '.mo' );
356 + }
357 + }
337 358
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 - }
359 + /**
360 + * For AJAX Only
361 + * @return void
362 + */
363 + public function ajax() {
364 + }
347 365
348 - // Delete the activation transient.
349 - $this->database->delete_transient( 'betterdocs_maybe_redirect' );
366 + /**
367 + * Create a plugin instance.
368 + *
369 + * @param mixed ...$args
370 + *
371 + * @return static
372 + *
373 + * @suppress PHP0441
374 + * @since 2.5.0
375 + */
376 + public static function get_instance() {
377 + if ( null == self::$_instance ) {
378 + self::$_instance = new self();
350 379
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 - }
380 + do_action( 'betterdocs_loaded' );
381 + }
360 382
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 - }
383 + return self::$_instance;
384 + }
368 385
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 - }
386 + /**
387 + * Hooked with `admin_init` action.
388 + * @return void
389 + */
390 + public function admin_init() {
391 + /**
392 + * Maybe Redirect
393 + * for setup related settings.
394 + */
395 + $this->maybe_redirect();
396 + }
376 397
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 - }
398 + /**
399 + * Summary of maybe_redirect
400 + * @return void
401 + */
402 + public function maybe_redirect() {
403 + // Bail if no activation transient is set.
404 + if ( ! $this->database->get_transient( 'betterdocs_maybe_redirect' ) ) {
405 + return;
406 + }
381 407
382 - return false;
383 - }
408 + // Delete the activation transient.
409 + $this->database->delete_transient( 'betterdocs_maybe_redirect' );
384 410
385 - public function chatbot_file() {
386 - return WP_PLUGIN_DIR . '/betterdocs-ai-chatbot/betterdocs-ai-chatbot.php';
387 - }
411 + if ( ! is_multisite() ) {
412 + $betterdocs_settings = get_option( 'betterdocs_settings' );
413 + if ( $betterdocs_settings ) {
414 + wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-settings' ), admin_url( 'admin.php' ) ) );
415 + } else {
416 + wp_safe_redirect( add_query_arg( array( 'page' => 'betterdocs-setup' ), admin_url( 'admin.php' ) ) );
417 + }
418 + // This runs at `admin_init` priority 0. Without exiting, the rest of
419 + // admin_init still executes and can mutate the very state the redirect
420 + // target depends on before the browser ever follows the Location header.
421 + exit;
422 + }
423 + }
388 424
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 - }
425 + /**
426 + * Is Pro Plugin Is Installed?
427 + * @return bool
428 + */
429 + public function is_pro_installed() {
430 + return $this->helper->get_plugins( 'betterdocs-pro/betterdocs-pro.php' );
431 + }
393 432
394 - return false;
395 - }
433 + /**
434 + * Is Pro Plugin Is Active?
435 + * @return bool
436 + */
437 + public function pro_file() {
438 + return WP_PLUGIN_DIR . '/betterdocs-pro/betterdocs-pro.php';
439 + }
396 440
397 - public function pro_version() {
398 - if ( ! $this->is_pro_active() ) {
399 - return false;
400 - }
441 + public function is_pro_active() {
442 + if ( file_exists( $this->pro_file() ) ) {
443 + return $this->helper->is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' );
444 + }
401 445
402 - if ( ! function_exists( 'get_plugin_data' ) ) {
403 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
404 - }
446 + return false;
447 + }
405 448
406 - $plugin_data = get_plugin_data( $this->pro_file() );
449 + public function chatbot_file() {
450 + return WP_PLUGIN_DIR . '/betterdocs-ai-chatbot/betterdocs-ai-chatbot.php';
451 + }
407 452
408 - return $plugin_data['Version'];
409 - }
453 + public function is_chatbot_active() {
454 + if ( file_exists( $this->chatbot_file() ) ) {
455 + return $this->helper->is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' );
456 + }
410 457
411 - /**
412 - * Get all the API initialized.
413 - * @return void
414 - */
415 - public function api_initialization() {
416 - $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
458 + return false;
459 + }
417 460
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 - }
461 + /**
462 + * Whether Pro provides the real API Documentation screen.
463 + *
464 + * Pro flips this via `betterdocs_pro_has_api_docs`; the class_exists() default
465 + * keeps the gate correct against a Pro build that predates that filter.
466 + *
467 + * @return bool
468 + */
469 + public function has_api_docs() {
470 + return (bool) apply_filters(
471 + 'betterdocs_pro_has_api_docs',
472 + class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\ApiReferences' )
473 + );
474 + }
423 475
424 - $classname = basename( $class, '.php' );
425 - $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
426 - $_api_class = $this->container->get( $classname );
476 + /**
477 + * Whether Free should show its locked API Docs teaser.
478 + *
479 + * Only without Pro. An older Pro has already paid, so they get nothing here —
480 + * they need a plugin update, not an upsell.
481 + *
482 + * @return bool
483 + */
484 + public function show_api_docs_teaser() {
485 + return ! $this->is_pro_active() && ! $this->has_api_docs();
486 + }
427 487
428 - if ( $_api_class instanceof BaseAPI ) {
429 - $_api_class->register();
430 - }
431 - }
432 - }
433 - }
488 + /**
489 + * Whether the real Glossaries screen is available (i.e. Pro is licensing it).
490 + *
491 + * Glossaries ships in Free's code but is Pro-licensed, so unlike API Docs there
492 + * is no separate Pro class to detect — the gate is simply Pro being active. Kept
493 + * behind a filter so Pro/add-ons can flip it explicitly, mirroring
494 + * `betterdocs_pro_has_api_docs`.
495 + *
496 + * @return bool
497 + */
498 + public function has_glossaries() {
499 + return (bool) apply_filters( 'betterdocs_pro_has_glossaries', $this->is_pro_active() );
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 + * Whether Free should show its locked Glossaries teaser.
504 + *
505 + * Only without Pro — an active Pro either has the feature or has simply left it
506 + * disabled in settings; neither case wants an upsell. Mirrors
507 + * show_api_docs_teaser().
508 + *
509 + * @return bool
510 + */
511 + public function show_glossary_teaser() {
512 + return ! $this->is_pro_active() && ! $this->has_glossaries();
513 + }
514 +
515 + /**
516 + * Whether Pro provides the real Content Intelligence screen.
517 + *
518 + * Pro flips this via `betterdocs_pro_has_content_intelligence`; the
519 + * class_exists() default keeps the gate correct against a Pro build that
520 + * predates that filter.
521 + *
522 + * @return bool
523 + */
524 + public function has_content_intelligence() {
525 + return (bool) apply_filters(
526 + 'betterdocs_pro_has_content_intelligence',
527 + class_exists( '\\WPDeveloper\\BetterDocsPro\\Core\\ContentIntelligenceService' )
528 + );
529 + }
530 +
531 + /**
532 + * Whether Free should show its locked Content Intelligence teaser.
533 + *
534 + * Only without Pro. An older Pro has already paid, so they get nothing here —
535 + * they need a plugin update, not an upsell.
536 + *
537 + * @return bool
538 + */
539 + public function show_content_intelligence_teaser() {
540 + return ! $this->is_pro_active() && ! $this->has_content_intelligence();
541 + }
542 +
543 + public function pro_version() {
544 + if ( ! $this->is_pro_active() ) {
545 + return false;
546 + }
547 +
548 + if ( ! function_exists( 'get_plugin_data' ) ) {
549 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
550 + }
551 +
552 + $plugin_data = get_plugin_data( $this->pro_file() );
553 +
554 + return $plugin_data[ 'Version' ];
555 + }
556 +
557 + /**
558 + * Get all the API initialized.
559 + * @return void
560 + */
561 + public function api_initialization() {
562 + $_api_classes = scandir( __DIR__ . DIRECTORY_SEPARATOR . 'REST' );
563 +
564 + if ( ! empty( $_api_classes ) && is_array( $_api_classes ) ) {
565 + foreach ( $_api_classes as $class ) {
566 + if ( '.' == $class || '..' == $class || strpos( $class, '.' ) === 0 ) {
567 + continue;
568 + }
569 +
570 + $classname = basename( $class, '.php' );
571 + $classname = '\\' . __NAMESPACE__ . "\\REST\\$classname";
572 + $_api_class = $this->container->get( $classname );
573 +
574 + if ( $_api_class instanceof BaseAPI ) {
575 + $_api_class->register();
576 + }
577 + }
578 + }
579 + }
580 +
581 + public function is_betterdocs_screen( $hook, $admin_check = true ): bool {
582 + /**
583 + * Filter the list of admin screen hook suffixes treated as BetterDocs
584 + * screens (controls whether the React admin bundle + styles load).
585 + * Pro/add-ons can register their own React pages here, e.g. the
586 + * Knowledge Base admin page.
587 + *
588 + * @param string[] $screens Full hook suffixes (e.g. betterdocs_page_betterdocs-foo).
589 + */
590 + $screens = apply_filters( 'betterdocs_admin_screens', array(
591 + 'toplevel_page_betterdocs-dashboard',
592 + 'toplevel_page_betterdocs-admin',
593 + 'admin_page_betterdocs-admin',
594 + 'betterdocs_page_betterdocs-admin',
595 + 'betterdocs_page_betterdocs-analytics',
596 + 'betterdocs_page_betterdocs-content-iq',
597 + 'betterdocs_page_betterdocs-settings',
598 + 'betterdocs_page_betterdocs-mcp',
599 + 'betterdocs_page_betterdocs-faq',
600 + 'betterdocs_page_betterdocs-glossaries',
445 601 'betterdocs_page_betterdocs-ai-chatbot',
446 - ];
602 + 'betterdocs_page_betterdocs-api-docs',
603 + 'betterdocs_page_betterdocs-doc-categories',
604 + 'betterdocs_page_betterdocs-doc-tags',
605 + ) );
447 606
448 - if ( $admin_check ) {
449 - if ( in_array( $hook, $screens ) ) {
450 - return true;
451 - }
607 + if ( $admin_check ) {
608 + if ( in_array( $hook, $screens ) ) {
609 + return true;
610 + }
452 611
453 - return false;
454 - }
612 + return false;
613 + }
455 614
456 - return false;
457 - }
615 + return false;
616 + }
458 617
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',
618 + public function get_betterdocs_screen() {
619 + $registered_screens = array(
620 + 'toplevel_page_betterdocs-dashboard',
621 + 'admin_page_betterdocs-admin',
622 + 'betterdocs_page_betterdocs-admin',
623 + 'betterdocs_page_betterdocs-settings',
624 + 'betterdocs_page_betterdocs-mcp',
625 + 'betterdocs_page_betterdocs-analytics',
626 + 'betterdocs_page_betterdocs-faq',
627 + 'betterdocs_page_betterdocs-glossaries',
468 628 'betterdocs_page_betterdocs-ai-chatbot',
469 - 'edit-docs',
470 - ];
629 + 'betterdocs_page_betterdocs-api-docs',
630 + 'betterdocs_page_betterdocs-doc-categories',
631 + 'betterdocs_page_betterdocs-doc-tags',
632 + 'edit-docs'
633 + );
471 634
472 - $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
635 + $current_screen_id = get_current_screen() != null ? get_current_screen()->id : '';
473 636
474 - if ( in_array( $current_screen_id, $registered_screens ) ) {
475 - return $current_screen_id;
476 - }
637 + if ( in_array( $current_screen_id, $registered_screens ) ) {
638 + return $current_screen_id;
639 + }
477 640
478 - return false;
479 - }
641 + return false;
642 + }
480 643 }