PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wp-media / mcp-oauth / inc / Bootstrap.php

Bootstrap.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at vendor/wp-media/mcp-oauth/inc/Bootstrap.php

257 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP OAuth Bootstrap.
4 *
5 * Single centralized entry point for the library. Consuming plugins call
6 * Bootstrap::instance() (recommended on the 'plugins_loaded' action); the
7 * first call wires the entire library to WordPress, every subsequent call
8 * (from the same or another consuming plugin) returns the same instance and
9 * binds nothing further.
10 */
11
12 declare( strict_types=1 );
13
14 namespace WPMedia\MCP\OAuth;
15
16 use WP\MCP\Core\McpAdapter;
17 use WPMedia\MCP\OAuth\Auth\AuthorizeCallback;
18 use WPMedia\MCP\OAuth\Auth\AuthorizeEndpoint;
19 use WPMedia\MCP\OAuth\Auth\CimdResolver;
20 use WPMedia\MCP\OAuth\Auth\ClaudeClientVerifier;
21 use WPMedia\MCP\OAuth\Auth\ConsentEndpoint;
22 use WPMedia\MCP\OAuth\Auth\Discovery\Endpoints as DiscoveryEndpoints;
23 use WPMedia\MCP\OAuth\Auth\Discovery\HealthCheck as DiscoveryHealthCheck;
24 use WPMedia\MCP\OAuth\Auth\RevokeEndpoint;
25 use WPMedia\MCP\OAuth\Auth\Rewrite;
26 use WPMedia\MCP\OAuth\Auth\Router;
27 use WPMedia\MCP\OAuth\Auth\SecretManager;
28 use WPMedia\MCP\OAuth\Auth\TokenEndpoint;
29 use WPMedia\MCP\OAuth\Transport\Server;
30 use WPMedia\MCP\OAuth\Transport\ServerRegistrar;
31 use WPMedia\MCP\OAuth\Views\Render;
32
33 /**
34 * Centralized single-instance bootstrap for the MCP OAuth library.
35 */
36 final class Bootstrap {
37
38 /**
39 * Bumped whenever any endpoint's or discovery document's rewrite regex changes.
40 */
41 private const REWRITE_VERSION = '1';
42
43 /**
44 * Option storing the rewrite-rules version last flushed for.
45 */
46 private const REWRITE_OPTION = 'wpmedia_mcp_oauth_rewrite_version';
47
48 /**
49 * The single instance.
50 *
51 * @var self
52 */
53 private static self $instance;
54
55 /**
56 * Whether register() has already run.
57 *
58 * @var bool
59 */
60 private static bool $initialized = false;
61
62 /**
63 * OAuth server context, shared across all wired collaborators.
64 *
65 * @var Context
66 */
67 private Context $context;
68
69 /**
70 * Return the single Bootstrap instance, wiring the library on first call.
71 *
72 * @return self
73 */
74 public static function instance(): self {
75 if ( ! isset( self::$instance ) ) {
76 self::$instance = new self();
77 self::$instance->register();
78 }
79
80 return self::$instance;
81 }
82
83 /**
84 * Private constructor — use instance().
85 */
86 private function __construct() {}
87
88 /**
89 * Singletons cannot be cloned.
90 *
91 * @return void
92 */
93 public function __clone() {
94 _doing_it_wrong( __METHOD__, 'Bootstrap is a singleton and cannot be cloned.', '1.0.0' );
95 }
96
97 /**
98 * Singletons cannot be unserialized.
99 *
100 * @return void
101 */
102 public function __wakeup() {
103 _doing_it_wrong( __METHOD__, 'Bootstrap is a singleton and cannot be unserialized.', '1.0.0' );
104 }
105
106 /**
107 * Wire the object graph and bind every WordPress hook.
108 *
109 * @return void
110 */
111 private function register(): void {
112 if ( self::$initialized ) {
113 return;
114 }
115
116 $this->context = new Context();
117
118 $this->register_auth_router();
119 $this->register_discovery( $this->context );
120 $this->register_discovery_health_check( $this->context );
121 $this->register_transport( $this->context );
122
123 add_action( 'init', [ SecretManager::class, 'ensure_secret' ], 5 );
124 add_action( 'init', [ $this, 'maybe_flush_rewrite_rules' ], 20 );
125
126 // Ensure the adapter is booted so it fires mcp_adapter_init on rest_api_init@15.
127 if ( class_exists( McpAdapter::class ) ) {
128 McpAdapter::instance();
129 }
130
131 self::$initialized = true;
132 }
133
134 /**
135 * Wire OAuth endpoint routing.
136 *
137 * @return void
138 */
139 private function register_auth_router(): void {
140 $authorize = new AuthorizeEndpoint( new CimdResolver( new ClaudeClientVerifier() ) );
141
142 $router = new Router(
143 new Rewrite(),
144 $authorize,
145 new AuthorizeCallback( new Render() ),
146 new TokenEndpoint(),
147 new ConsentEndpoint(),
148 new RevokeEndpoint(),
149 $this->context
150 );
151
152 add_action( 'init', [ $router, 'register_rewrite_rules' ] );
153 add_filter( 'query_vars', [ $router, 'add_query_vars' ] );
154 add_action( 'template_redirect', [ $router, 'handle_request' ] );
155 add_action( 'wp_delete_application_password', [ $router, 'purge_refresh_jti_meta' ], 10, 2 );
156 }
157
158 /**
159 * Wire the .well-known discovery documents.
160 *
161 * @param Context $context OAuth server context.
162 * @return void
163 */
164 private function register_discovery( Context $context ): void {
165 $discovery = new DiscoveryEndpoints( $context );
166
167 add_action( 'init', [ $discovery, 'add_rewrite_rules' ] );
168 add_filter( 'query_vars', [ $discovery, 'add_query_vars' ] );
169 add_action( 'template_redirect', [ $discovery, 'handle_request' ] );
170 }
171
172 /**
173 * Wire the Site Health self-check for the .well-known discovery documents.
174 *
175 * @param Context $context OAuth server context.
176 * @return void
177 */
178 private function register_discovery_health_check( Context $context ): void {
179 $health_check = new DiscoveryHealthCheck( $context );
180
181 add_filter( 'site_status_tests', [ $health_check, 'add_test' ] );
182 }
183
184 /**
185 * Wire MCP server + abilities registration.
186 *
187 * @param Context $context OAuth server context.
188 * @return void
189 */
190 private function register_transport( Context $context ): void {
191 $registrar = new ServerRegistrar( new Server( $context ), $context );
192
193 add_action( 'wp_abilities_api_categories_init', [ $registrar, 'ensure_default_category' ] );
194 add_action( 'wp_abilities_api_init', [ $registrar, 'ensure_shared_abilities_registered' ] );
195 add_action( 'mcp_adapter_init', [ $registrar, 'register_server' ] );
196 }
197
198 /**
199 * Lazily flush rewrite rules once per REWRITE_VERSION bump.
200 *
201 * Runs after rewrite rules are (re-)registered on the same 'init' action
202 * (priority 10), so the rules exist before being persisted.
203 *
204 * @return void
205 */
206 public function maybe_flush_rewrite_rules(): void {
207 if ( ! $this->context->is_enabled() ) {
208 return;
209 }
210
211 if ( ! $this->needs_rewrite_flush() ) {
212 return;
213 }
214
215 flush_rewrite_rules( false );
216 update_option( self::REWRITE_OPTION, self::REWRITE_VERSION, false );
217 }
218
219 /**
220 * Whether the OAuth rewrite rules need to be (re-)persisted.
221 *
222 * Self-heals cases the version flag alone cannot detect: a fresh site, a
223 * filter/snippet that enables the server only after init@20 on the previous
224 * load, or our rules dropped from the persisted set. When pretty permalinks
225 * are off, the rules can never be persisted, so a flag match alone is used
226 * to avoid flushing on every request.
227 *
228 * @return bool
229 */
230 private function needs_rewrite_flush(): bool {
231 if ( get_option( self::REWRITE_OPTION ) !== self::REWRITE_VERSION ) {
232 return true;
233 }
234
235 // Plain permalinks: no pretty rules to check; a flag match is enough.
236 if ( '' === (string) get_option( 'permalink_structure' ) ) {
237 return false;
238 }
239
240 $rules = get_option( 'rewrite_rules' );
241
242 return ! is_array( $rules ) || ! array_key_exists( Rewrite::AUTHORIZE_RULE, $rules );
243 }
244
245 /**
246 * Force the next 'init' to re-flush rewrite rules.
247 *
248 * Call this whenever whatever flips the `wpmedia_mcp_oauth_server_enabled`
249 * filter changes state — a version-flag match alone cannot detect that.
250 *
251 * @return void
252 */
253 public static function schedule_rewrite_flush(): void {
254 delete_option( self::REWRITE_OPTION );
255 }
256 }
257