PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.1
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.1
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 3.5.2 All 199 releases
betterdocs / includes / Abilities / ProState.php

ProState.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.1, at includes/Abilities/ProState.php

335 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BetterDocs Pro state probe.
4 *
5 * @package BetterDocs
6 * @since 4.9.0
7 */
8
9 namespace WPDeveloper\BetterDocs\Abilities;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 /**
16 * One per-request answer to "what can this site actually do?".
17 *
18 * Three things decide whether a knowledge-base or analytics tool can run: is Pro
19 * on disk, is it active, and — for the knowledge-base family — is Multiple
20 * Knowledge Base switched on. Each of those is a different sentence to an agent
21 * and a different fix, so they are probed once, collapsed into a `state` slug,
22 * and reused: by the stubs' refusals, by the live descriptions in `tools/list`,
23 * and by `bd-get-status`.
24 *
25 * The probe is memoised for the request. `tools/list` describes 28 tools from a
26 * single probe — {@see self::probe_count()} exists so that stays measurable — and
27 * `reset()` clears it for tests.
28 *
29 * The licence is **reported, never enforced** (ADR-004): BetterDocs Pro's own
30 * features run whether or not `betterdocs_pro_software__license_status` is
31 * `valid`, so an MCP server that refused on it would be stricter than the
32 * product it speaks for. It is also the *last* thing the state reports, so an
33 * unlicensed site still gets the actionable answer when something else is
34 * genuinely in the way (ADR-034).
35 *
36 * @since 4.9.0
37 */
38 final class ProState {
39
40 /**
41 * Pro's plugin basename, as WordPress keys it.
42 *
43 * @since 4.9.0
44 */
45 const PRO_BASENAME = 'betterdocs-pro/betterdocs-pro.php';
46
47 /**
48 * Option Pro writes its licence status into.
49 *
50 * @since 4.9.0
51 */
52 const LICENSE_OPTION = 'betterdocs_pro_software__license_status';
53
54 /**
55 * Memoised probe (everything except `state`, which depends on the caller's
56 * feature).
57 *
58 * @since 4.9.0
59 *
60 * @var array|null
61 */
62 private static $probe = null;
63
64 /**
65 * How many times the site was really probed this request.
66 *
67 * @since 4.9.0
68 *
69 * @var int
70 */
71 private static $probe_count = 0;
72
73 /**
74 * The Pro state as this request sees it.
75 *
76 * @since 4.9.0
77 *
78 * @param bool $kb_feature Whether the caller needs the Multiple Knowledge Base
79 * feature. Analytics does not, so it must not be told
80 * a setting it does not use is in its way.
81 * @return array {
82 * @type bool $installed Pro's plugin file is on disk.
83 * @type bool $active Pro is active.
84 * @type string|null $version Pro's version when active, else null.
85 * @type string $license_status Raw licence status option ('' when unset).
86 * @type bool $licensed Licence status is exactly `valid`.
87 * @type bool $multiple_kb The Multiple Knowledge Base setting is on.
88 * @type bool $kb_taxonomy_registered `knowledge_base` is a registered taxonomy.
89 * @type string $state One of `pro_not_installed`, `pro_not_active`,
90 * `pro_active_setting_off`, `pro_unlicensed`, `ok`.
91 * }
92 */
93 public static function get( bool $kb_feature = true ): array {
94 $state = self::probe();
95
96 $state['state'] = self::resolve_state( $state, $kb_feature );
97
98 return $state;
99 }
100
101 /**
102 * Whether a state stops the feature from running at all.
103 *
104 * `pro_unlicensed` is deliberately **not** blocking (ADR-004) — it is
105 * reported in descriptions and in `bd-get-status`, and execution proceeds.
106 *
107 * @since 4.9.0
108 *
109 * @param array $state A {@see self::get()} result.
110 * @return bool
111 */
112 public static function is_blocking( array $state ): bool {
113 $slug = isset( $state['state'] ) ? (string) $state['state'] : '';
114
115 return in_array( $slug, [ 'pro_not_installed', 'pro_not_active', 'pro_active_setting_off' ], true );
116 }
117
118 /**
119 * Appends what is true on *this* site to a tool's base description.
120 *
121 * Used by every Pro stub and by Pro's own knowledge-base
122 * abilities, so the sentence an agent reads in `tools/list` is the same one
123 * it would get as a refusal — it can decide not to call the tool at all, or
124 * fix the setting first.
125 *
126 * @since 4.9.0
127 *
128 * @param string $base Static description of the tool.
129 * @param array $state A {@see self::get()} result.
130 * @param string $setting_tool_hint MCP tool name that can turn the setting on.
131 * @return string
132 */
133 public static function describe( string $base, array $state, string $setting_tool_hint = 'bd-update-settings' ): string {
134 $slug = isset( $state['state'] ) ? (string) $state['state'] : 'ok';
135
136 switch ( $slug ) {
137 case 'pro_not_installed':
138 case 'pro_not_active':
139 $suffix = __( 'Requires BetterDocs Pro, which is not active on this site.', 'betterdocs' );
140 break;
141
142 case 'pro_unlicensed':
143 $status = isset( $state['license_status'] ) ? (string) $state['license_status'] : '';
144
145 $suffix = sprintf(
146 /* translators: %s: licence status reported by BetterDocs Pro. */
147 __( 'BetterDocs Pro is active but its licence is not valid (status: %s); the tool still works.', 'betterdocs' ),
148 '' !== $status ? $status : __( 'none', 'betterdocs' )
149 );
150 break;
151
152 case 'pro_active_setting_off':
153 $suffix = sprintf(
154 /* translators: %s: MCP tool name that writes settings. */
155 __( 'Multiple Knowledge Base is off — enable it with %s ({"multiple_kb": true}); it takes effect from the next request.', 'betterdocs' ),
156 $setting_tool_hint
157 );
158 break;
159
160 default:
161 return $base;
162 }
163
164 $base = rtrim( $base );
165
166 return '' !== $base ? $base . ' ' . $suffix : $suffix;
167 }
168
169 /**
170 * How many real probes have happened this request.
171 *
172 * `tools/list` must describe every tool from one probe; the acceptance
173 * battery asserts this stays at 1 across a full listing.
174 *
175 * @since 4.9.0
176 *
177 * @return int
178 */
179 public static function probe_count(): int {
180 return self::$probe_count;
181 }
182
183 /**
184 * Clears the memoised probe and the counter. Tests only.
185 *
186 * @since 4.9.0
187 *
188 * @return void
189 */
190 public static function reset(): void {
191 self::$probe = null;
192 self::$probe_count = 0;
193 }
194
195 /**
196 * Reads the site once.
197 *
198 * `installed` is answered before `active` on purpose: `is_pro_active()` goes
199 * through `Helper::is_plugin_active()`, which `include_once`s
200 * `wp-admin/includes/plugin.php` and so *defines* `get_plugins()` as a side
201 * effect. Probing in this order keeps the branch taken here the same whether
202 * or not something else already loaded that file.
203 *
204 * @since 4.9.0
205 *
206 * @return array
207 */
208 private static function probe(): array {
209 if ( null !== self::$probe ) {
210 return self::$probe;
211 }
212
213 ++self::$probe_count;
214
215 $installed = self::pro_installed();
216 $active = self::pro_active();
217 $license = (string) get_option( self::LICENSE_OPTION, '' );
218
219 self::$probe = [
220 'installed' => $installed,
221 'active' => $active,
222 'version' => defined( 'BETTERDOCS_PRO_VERSION' ) ? (string) BETTERDOCS_PRO_VERSION : null,
223 'license_status' => $license,
224 'licensed' => 'valid' === $license,
225 'multiple_kb' => (bool) self::setting( 'multiple_kb' ),
226 'kb_taxonomy_registered' => function_exists( 'taxonomy_exists' ) && taxonomy_exists( 'knowledge_base' )
227 ];
228
229 return self::$probe;
230 }
231
232 /**
233 * Collapses the probe into one state slug.
234 *
235 * Precedence: not installed, not active, setting off, unlicensed, ok — most
236 * blocking first, and the licence last of the four because it blocks nothing.
237 *
238 * `pro_active_setting_off` deliberately outranks `pro_unlicensed`, which is
239 * the one place this diverges from the written plan (ADR-034). ADR-004 says
240 * the licence is reported and never enforced; if the licence outranked the
241 * setting then on an unlicensed site — the ordinary state of a fresh Pro
242 * install, and what the rig runs — a knowledge-base tool with Multiple
243 * Knowledge Base switched off would answer "the licence is not valid"
244 * instead of the actionable "switch the setting on": the licence would
245 * quietly change what an agent is told. It still surfaces on its own
246 * whenever nothing else is in the way.
247 *
248 * @since 4.9.0
249 *
250 * @param array $probe Probe fields.
251 * @param bool $kb_feature Whether the caller needs Multiple Knowledge Base.
252 * @return string
253 */
254 private static function resolve_state( array $probe, bool $kb_feature ): string {
255 if ( empty( $probe['installed'] ) && empty( $probe['active'] ) ) {
256 return 'pro_not_installed';
257 }
258
259 if ( empty( $probe['active'] ) ) {
260 return 'pro_not_active';
261 }
262
263 if ( $kb_feature && empty( $probe['multiple_kb'] ) ) {
264 return 'pro_active_setting_off';
265 }
266
267 if ( empty( $probe['licensed'] ) ) {
268 return 'pro_unlicensed';
269 }
270
271 return 'ok';
272 }
273
274 /**
275 * Whether Pro's plugin file is on disk, active or not.
276 *
277 * `get_plugins()` is the accurate answer (it is what the Plugins screen
278 * reads) but only exists once `wp-admin/includes/plugin.php` is loaded, which
279 * is not the case on a front-end request. The file check is the fallback, and
280 * is what a normal MCP request uses.
281 *
282 * @since 4.9.0
283 *
284 * @return bool
285 */
286 private static function pro_installed(): bool {
287 if ( function_exists( 'get_plugins' ) ) {
288 $plugins = get_plugins();
289
290 return is_array( $plugins ) && isset( $plugins[ self::PRO_BASENAME ] );
291 }
292
293 return defined( 'WP_PLUGIN_DIR' ) && file_exists( WP_PLUGIN_DIR . '/' . self::PRO_BASENAME );
294 }
295
296 /**
297 * Whether Pro is active, asked of BetterDocs itself rather than of
298 * WordPress, so multisite network activation and any future override are
299 * answered the same way the rest of the plugin answers them.
300 *
301 * @since 4.9.0
302 *
303 * @return bool
304 */
305 private static function pro_active(): bool {
306 if ( ! function_exists( 'betterdocs' ) ) {
307 return false;
308 }
309
310 return (bool) betterdocs()->is_pro_active();
311 }
312
313 /**
314 * Reads one BetterDocs setting, tolerating a plugin that has not booted.
315 *
316 * @since 4.9.0
317 *
318 * @param string $key Setting key.
319 * @return mixed Null when settings are unreachable.
320 */
321 private static function setting( string $key ) {
322 if ( ! function_exists( 'betterdocs' ) ) {
323 return null;
324 }
325
326 $plugin = betterdocs();
327
328 if ( ! is_object( $plugin ) || ! isset( $plugin->settings ) || ! is_object( $plugin->settings ) ) {
329 return null;
330 }
331
332 return $plugin->settings->get( $key );
333 }
334 }
335