PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.2
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 2.3.2, at vendor/wp-media/mcp-oauth/inc/Bootstrap.php

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