PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Admin / GraphiQL / GraphiQL.php

GraphiQL.php in WPGraphQL trunk, at src/Admin/GraphiQL/GraphiQL.php

402 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Admin\GraphiQL;
4
5 use WP_Admin_Bar;
6
7 /**
8 * Class GraphiQL
9 *
10 * Sets up GraphiQL in the WordPress Admin
11 *
12 * @package WPGraphQL\Admin\GraphiQL
13 */
14 class GraphiQL {
15
16 /**
17 * @var bool Whether GraphiQL is enabled
18 */
19 protected $is_enabled = false;
20
21 /**
22 * Whether JavaScript build assets are available.
23 *
24 * The GraphiQL IDE requires compiled JavaScript assets from the /build directory.
25 * These assets are not committed to the repository and must be generated by running
26 * `npm run build`. Users who install via WordPress.org or GitHub releases get
27 * pre-built assets, but those who clone the repo or install via Composer need
28 * to build them manually.
29 *
30 * @var bool
31 */
32 protected $build_assets_available = false;
33
34 /**
35 * Core assets required for GraphiQL to function.
36 *
37 * This configuration serves as the single source of truth for:
38 * 1. Validating that required build files exist
39 * 2. Enqueueing scripts and styles with correct dependencies
40 *
41 * Each asset defines:
42 * - 'file': Base filename in /build (without extension)
43 * - 'has_style': Whether a .css file accompanies the .js
44 * - 'script_deps': WP script handles this asset depends on
45 * - 'style_deps': WP style handles this asset's CSS depends on
46 */
47 protected const CORE_ASSETS = [
48 'wp-graphiql' => [
49 'file' => 'index',
50 'has_style' => false,
51 'script_deps' => [],
52 'style_deps' => [],
53 ],
54 'wp-graphiql-app' => [
55 'file' => 'app',
56 'has_style' => true,
57 'script_deps' => [ 'wp-graphiql' ],
58 'style_deps' => [ 'wp-components' ],
59 ],
60 ];
61
62 /**
63 * Extension assets that enhance GraphiQL with additional features.
64 *
65 * Unlike core assets, these are optional - GraphiQL will still function
66 * if these assets are missing. They're loaded via the 'enqueue_graphiql_extension'
67 * action to demonstrate how third-party extensions can hook into GraphiQL.
68 *
69 * Built-in extensions include:
70 * - Auth Switch: Toggle between authenticated/public API requests
71 * - Query Composer: Build queries using a visual form interface
72 * - Fullscreen Toggle: Expand GraphiQL to fill the browser window
73 */
74 protected const EXTENSION_ASSETS = [
75 'wp-graphiql-auth-switch' => [
76 'file' => 'graphiqlAuthSwitch',
77 'has_style' => false,
78 'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
79 'style_deps' => [],
80 ],
81 'wp-graphiql-query-composer' => [
82 'file' => 'graphiqlQueryComposer',
83 'has_style' => true,
84 'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
85 'style_deps' => [ 'wp-components' ],
86 ],
87 'wp-graphiql-fullscreen-toggle' => [
88 'file' => 'graphiqlFullscreenToggle',
89 'has_style' => true,
90 'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
91 'style_deps' => [ 'wp-components' ],
92 ],
93 ];
94
95 /**
96 * Initialize Admin functionality for WPGraphQL
97 *
98 * @return void
99 */
100 public function init() {
101 $this->is_enabled = get_graphql_setting( 'graphiql_enabled' ) !== 'off';
102 $this->build_assets_available = $this->check_build_assets();
103
104 /**
105 * If GraphiQL is disabled, don't set it up in the Admin
106 */
107 if ( ! $this->is_enabled ) {
108 return;
109 }
110
111 // Register the admin page
112 add_action( 'admin_menu', [ $this, 'register_admin_page' ], 9 );
113 add_action( 'admin_bar_menu', [ $this, 'register_admin_bar_menu' ], 100 );
114
115 // Only enqueue assets if they exist - the render method handles showing
116 // a helpful message when assets are missing
117 if ( ! $this->build_assets_available ) {
118 return;
119 }
120
121 // Enqueue GraphiQL React App
122 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_graphiql' ] );
123
124 // Enqueue built-in extensions via the extension action
125 add_action( 'enqueue_graphiql_extension', [ $this, 'enqueue_builtin_extensions' ] );
126 }
127
128 /**
129 * Check if the core JavaScript build assets are available.
130 *
131 * The /build directory is gitignored because compiled assets don't belong in
132 * version control. This means users who install WPGraphQL via:
133 * - Git clone
134 * - Composer (from GitHub)
135 *
136 * ...won't have the /build directory and need to run `npm ci && npm run build`.
137 *
138 * Users who install via WordPress.org or download a GitHub release ZIP get
139 * pre-built assets because the release workflow runs `npm run build` before
140 * packaging.
141 *
142 * This check prevents fatal errors from trying to include non-existent asset
143 * files and allows us to show a helpful message instead.
144 *
145 * @return bool True if all required build assets exist, false otherwise.
146 */
147 protected function check_build_assets(): bool {
148 foreach ( self::CORE_ASSETS as $config ) {
149 $base_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $config['file'];
150
151 // Check required files: asset manifest and JS file
152 if ( ! file_exists( $base_path . '.asset.php' ) || ! file_exists( $base_path . '.js' ) ) {
153 return false;
154 }
155
156 // If asset has a style, check for CSS file too
157 if ( $config['has_style'] && ! file_exists( $base_path . '.css' ) ) {
158 return false;
159 }
160 }
161
162 return true;
163 }
164
165 /**
166 * Registers admin bar menu
167 *
168 * @param \WP_Admin_Bar $admin_bar The Admin Bar Instance
169 *
170 * @return void
171 */
172 public function register_admin_bar_menu( WP_Admin_Bar $admin_bar ) {
173
174 if ( 'off' === get_graphql_setting( 'graphiql_enabled' ) ) {
175 return;
176 }
177
178 if ( ! current_user_can( 'manage_options' ) ) {
179 return;
180 }
181
182 if ( 'off' === get_graphql_setting( 'show_graphiql_link_in_admin_bar' ) ) {
183 return;
184 }
185
186 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgNDAwIj48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNNTcuNDY4IDMwMi42NmwtMTQuMzc2LTguMyAxNjAuMTUtMjc3LjM4IDE0LjM3NiA4LjN6Ii8+PHBhdGggZmlsbD0iI0UxMDA5OCIgZD0iTTM5LjggMjcyLjJoMzIwLjN2MTYuNkgzOS44eiIvPjxwYXRoIGZpbGw9IiNFMTAwOTgiIGQ9Ik0yMDYuMzQ4IDM3NC4wMjZsLTE2MC4yMS05Mi41IDguMy0xNC4zNzYgMTYwLjIxIDkyLjV6TTM0NS41MjIgMTMyLjk0N2wtMTYwLjIxLTkyLjUgOC4zLTE0LjM3NiAxNjAuMjEgOTIuNXoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNNTQuNDgyIDEzMi44ODNsLTguMy0xNC4zNzUgMTYwLjIxLTkyLjUgOC4zIDE0LjM3NnoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMzQyLjU2OCAzMDIuNjYzbC0xNjAuMTUtMjc3LjM4IDE0LjM3Ni04LjMgMTYwLjE1IDI3Ny4zOHpNNTIuNSAxMDcuNWgxNi42djE4NUg1Mi41ek0zMzAuOSAxMDcuNWgxNi42djE4NWgtMTYuNnoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMjAzLjUyMiAzNjdsLTcuMjUtMTIuNTU4IDEzOS4zNC04MC40NSA3LjI1IDEyLjU1N3oiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMzY5LjUgMjk3LjljLTkuNiAxNi43LTMxIDIyLjQtNDcuNyAxMi44LTE2LjctOS42LTIyLjQtMzEtMTIuOC00Ny43IDkuNi0xNi43IDMxLTIyLjQgNDcuNy0xMi44IDE2LjggOS43IDIyLjUgMzEgMTIuOCA0Ny43TTkwLjkgMTM3Yy05LjYgMTYuNy0zMSAyMi40LTQ3LjcgMTIuOC0xNi43LTkuNi0yMi40LTMxLTEyLjgtNDcuNyA5LjYtMTYuNyAzMS0yMi40IDQ3LjctMTIuOCAxNi43IDkuNyAyMi40IDMxIDEyLjggNDcuN00zMC41IDI5Ny45Yy05LjYtMTYuNy0zLjktMzggMTIuOC00Ny43IDE2LjctOS42IDM4LTMuOSA0Ny43IDEyLjggOS42IDE2LjcgMy45IDM4LTEyLjggNDcuNy0xNi44IDkuNi0zOC4xIDMuOS00Ny43LTEyLjhNMzA5LjEgMTM3Yy05LjYtMTYuNy0zLjktMzggMTIuOC00Ny43IDE2LjctOS42IDM4LTMuOSA0Ny43IDEyLjggOS42IDE2LjcgMy45IDM4LTEyLjggNDcuNy0xNi43IDkuNi0zOC4xIDMuOS00Ny43LTEyLjhNMjAwIDM5NS44Yy0xOS4zIDAtMzQuOS0xNS42LTM0LjktMzQuOSAwLTE5LjMgMTUuNi0zNC45IDM0LjktMzQuOSAxOS4zIDAgMzQuOSAxNS42IDM0LjkgMzQuOSAwIDE5LjItMTUuNiAzNC45LTM0LjkgMzQuOU0yMDAgNzRjLTE5LjMgMC0zNC45LTE1LjYtMzQuOS0zNC45IDAtMTkuMyAxNS42LTM0LjkgMzQuOS0zNC45IDE5LjMgMCAzNC45IDE1LjYgMzQuOSAzNC45IDAgMTkuMy0xNS42IDM0LjktMzQuOSAzNC45Ii8+PC9zdmc+';
187
188 $icon = sprintf(
189 '<span class="custom-icon" style="
190 background-image:url(\'%s\'); float:left; width:22px !important; height:22px !important;
191 margin-left: 5px !important; margin-top: 5px !important; margin-right: 5px !important;
192 "></span>',
193 $icon_url
194 );
195
196 $admin_bar->add_menu(
197 [
198 'id' => 'graphiql-ide',
199 'title' => $icon . __( 'GraphiQL IDE', 'wp-graphql' ),
200 'href' => trailingslashit( admin_url() ) . 'admin.php?page=graphiql-ide',
201 ]
202 );
203 }
204
205 /**
206 * Register the admin page as a subpage
207 *
208 * @return void
209 */
210 public function register_admin_page() {
211 $svg_file = file_get_contents( WPGRAPHQL_PLUGIN_DIR . '/src/assets/wpgraphql-elephant.svg' ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- reads a bundled plugin file, not a remote URL
212
213 if ( false === $svg_file ) {
214 return;
215 }
216
217 $svg_base64 = base64_encode( $svg_file );
218
219 // Top level menu page should be labeled GraphQL
220 add_menu_page(
221 __( 'GraphQL', 'wp-graphql' ),
222 __( 'GraphQL', 'wp-graphql' ),
223 'manage_options',
224 'graphiql-ide',
225 [ $this, 'render_graphiql_admin_page' ],
226 'data:image/svg+xml;base64,' . $svg_base64
227 );
228
229 // Sub menu should be labeled GraphiQL IDE
230 add_submenu_page(
231 'graphiql-ide',
232 __( 'GraphiQL IDE', 'wp-graphql' ),
233 __( 'GraphiQL IDE', 'wp-graphql' ),
234 'manage_options',
235 'graphiql-ide',
236 [ $this, 'render_graphiql_admin_page' ]
237 );
238 }
239
240 /**
241 * Render the markup for the GraphiQL admin page.
242 *
243 * When build assets are available, this outputs a container div that the
244 * React app will mount to. The "Loading..." text is shown briefly while
245 * JavaScript initializes.
246 *
247 * When build assets are missing, we show helpful instructions instead of
248 * a broken interface, guiding developers to either build the assets or
249 * download a pre-built release.
250 *
251 * @return void
252 */
253 public function render_graphiql_admin_page() {
254 // If build assets are missing, show instructions instead of the loading message
255 if ( ! $this->build_assets_available ) {
256 $message = sprintf(
257 /* translators: 1: npm ci command, 2: npm run build command, 3: documentation URL */
258 __( 'The GraphiQL IDE requires JavaScript assets that need to be built. Please run %1$s followed by %2$s in the plugin directory, or <a href="%3$s" target="_blank">download a release</a> that includes pre-built assets.', 'wp-graphql' ),
259 '<code>npm ci</code>',
260 '<code>npm run build</code>',
261 'https://github.com/wp-graphql/wp-graphql/releases'
262 );
263
264 $rendered = '<div class="wrap"><div class="notice notice-warning inline"><p>' . $message . '</p></div></div>';
265 } else {
266 /**
267 * Filters the rendered GraphiQL admin page markup.
268 *
269 * @param string $rendered The rendered HTML markup for the GraphiQL admin page.
270 *
271 * @hookGroup settings
272 * @since 1.6.9
273 */
274 $rendered = apply_filters( 'graphql_render_admin_page', '<div class="wrap" dir="ltr"><div id="graphiql" class="graphiql-container">Loading ...</div></div>' );
275 }
276
277 echo wp_kses_post( $rendered );
278 }
279
280 /**
281 * Enqueue the core scripts and styles for the WPGraphiQL app.
282 *
283 * This loads the foundational GraphiQL interface. The core assets provide:
284 * - wp-graphiql: The base library exposing GraphQL utilities and hooks
285 * - wp-graphiql-app: The React application that renders the IDE
286 *
287 * After core assets are loaded, we fire 'enqueue_graphiql_extension' to allow
288 * built-in and third-party extensions to add their functionality.
289 *
290 * @return void
291 */
292 public function enqueue_graphiql() {
293 if ( null === get_current_screen() || ! strpos( get_current_screen()->id, 'graphiql' ) ) {
294 return;
295 }
296
297 // Enqueue core assets
298 foreach ( self::CORE_ASSETS as $handle => $config ) {
299 $this->enqueue_asset( $handle, $config );
300 }
301
302 // Localize the main script with settings
303 wp_localize_script(
304 'wp-graphiql',
305 'wpGraphiQLSettings',
306 [
307 'nonce' => wp_create_nonce( 'wp_rest' ),
308 'graphqlEndpoint' => trailingslashit( site_url() ) . 'index.php?' . graphql_get_endpoint(),
309 'avatarUrl' => 0 !== get_current_user_id() ? get_avatar_url( get_current_user_id() ) : null,
310 'externalFragments' => apply_filters( 'graphiql_external_fragments', [] ),
311 ]
312 );
313
314 // Extensions looking to extend GraphiQL can hook in here,
315 // after the window object is established, but before the App renders.
316 /**
317 * Fires before GraphiQL extensions are enqueued.
318 *
319 * @hookGroup settings
320 * @since 1.7.0
321 */
322 do_action( 'enqueue_graphiql_extension' );
323 }
324
325 /**
326 * Enqueue the built-in GraphiQL extensions.
327 *
328 * These extensions ship with WPGraphQL but are loaded through the same
329 * 'enqueue_graphiql_extension' hook that third-party extensions use. This
330 * demonstrates the extension API and ensures our built-in features don't
331 * have any special privileges over community extensions.
332 *
333 * Extensions are loaded with graceful degradation - if an extension's build
334 * files are missing (e.g., partial build), we skip it rather than breaking
335 * the entire GraphiQL interface.
336 *
337 * @return void
338 */
339 public function enqueue_builtin_extensions() {
340 foreach ( self::EXTENSION_ASSETS as $handle => $config ) {
341 // Skip extensions whose assets don't exist (graceful degradation)
342 $asset_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $config['file'] . '.asset.php';
343 if ( ! file_exists( $asset_path ) ) {
344 continue;
345 }
346
347 $this->enqueue_asset( $handle, $config );
348 }
349 }
350
351 /**
352 * Enqueue a single asset (script and optionally style) based on configuration.
353 *
354 * This helper centralizes the asset enqueueing logic to avoid repetition.
355 * It handles:
356 * - Loading the webpack-generated .asset.php manifest for dependencies/version
357 * - Merging manifest dependencies with our explicit dependencies
358 * - Enqueueing both JS and CSS (when applicable) with consistent patterns
359 *
360 * The .asset.php files are generated by @wordpress/scripts during `npm run build`
361 * and contain the list of wp-* dependencies the script needs, plus a content
362 * hash for cache busting.
363 *
364 * @param non-empty-string $handle The WordPress script/style handle.
365 * @param array<string,mixed> $config The asset configuration from CORE_ASSETS or EXTENSION_ASSETS.
366 */
367 protected function enqueue_asset( string $handle, array $config ): void {
368 $file = $config['file'];
369 $asset_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $file . '.asset.php';
370
371 // Asset file must exist
372 if ( ! file_exists( $asset_path ) ) {
373 return;
374 }
375
376 // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Path is constructed from WPGRAPHQL_PLUGIN_DIR constant + hardcoded string, validated with file_exists()
377 $asset_file = include $asset_path;
378
379 // Enqueue the script
380 wp_enqueue_script(
381 $handle,
382 WPGRAPHQL_PLUGIN_URL . 'build/' . $file . '.js',
383 array_merge( $config['script_deps'], $asset_file['dependencies'] ),
384 $asset_file['version'],
385 true
386 );
387
388 // Set up script translations for i18n
389 wp_set_script_translations( $handle, 'wp-graphql', WPGRAPHQL_PLUGIN_DIR . 'languages' );
390
391 // Enqueue the style if this asset has one
392 if ( ! empty( $config['has_style'] ) ) {
393 wp_enqueue_style(
394 $handle,
395 WPGRAPHQL_PLUGIN_URL . 'build/' . $file . '.css',
396 $config['style_deps'],
397 $asset_file['version']
398 );
399 }
400 }
401 }
402