File: /var/www/html/wp-content/plugins/wp-graphql/src/Admin/GraphiQL/GraphiQL.php
<?php
namespace WPGraphQL\Admin\GraphiQL;
use WP_Admin_Bar;
/**
* Class GraphiQL
*
* Sets up GraphiQL in the WordPress Admin
*
* @package WPGraphQL\Admin\GraphiQL
*/
class GraphiQL {
/**
* @var bool Whether GraphiQL is enabled
*/
protected $is_enabled = false;
/**
* Whether JavaScript build assets are available.
*
* The GraphiQL IDE requires compiled JavaScript assets from the /build directory.
* These assets are not committed to the repository and must be generated by running
* `npm run build`. Users who install via WordPress.org or GitHub releases get
* pre-built assets, but those who clone the repo or install via Composer need
* to build them manually.
*
* @var bool
*/
protected $build_assets_available = false;
/**
* Core assets required for GraphiQL to function.
*
* This configuration serves as the single source of truth for:
* 1. Validating that required build files exist
* 2. Enqueueing scripts and styles with correct dependencies
*
* Each asset defines:
* - 'file': Base filename in /build (without extension)
* - 'has_style': Whether a .css file accompanies the .js
* - 'script_deps': WP script handles this asset depends on
* - 'style_deps': WP style handles this asset's CSS depends on
*/
protected const CORE_ASSETS = [
'wp-graphiql' => [
'file' => 'index',
'has_style' => false,
'script_deps' => [],
'style_deps' => [],
],
'wp-graphiql-app' => [
'file' => 'app',
'has_style' => true,
'script_deps' => [ 'wp-graphiql' ],
'style_deps' => [ 'wp-components' ],
],
];
/**
* Extension assets that enhance GraphiQL with additional features.
*
* Unlike core assets, these are optional - GraphiQL will still function
* if these assets are missing. They're loaded via the 'enqueue_graphiql_extension'
* action to demonstrate how third-party extensions can hook into GraphiQL.
*
* Built-in extensions include:
* - Auth Switch: Toggle between authenticated/public API requests
* - Query Composer: Build queries using a visual form interface
* - Fullscreen Toggle: Expand GraphiQL to fill the browser window
*/
protected const EXTENSION_ASSETS = [
'wp-graphiql-auth-switch' => [
'file' => 'graphiqlAuthSwitch',
'has_style' => false,
'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
'style_deps' => [],
],
'wp-graphiql-query-composer' => [
'file' => 'graphiqlQueryComposer',
'has_style' => true,
'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
'style_deps' => [ 'wp-components' ],
],
'wp-graphiql-fullscreen-toggle' => [
'file' => 'graphiqlFullscreenToggle',
'has_style' => true,
'script_deps' => [ 'wp-graphiql', 'wp-graphiql-app' ],
'style_deps' => [ 'wp-components' ],
],
];
/**
* Initialize Admin functionality for WPGraphQL
*
* @return void
*/
public function init() {
$this->is_enabled = get_graphql_setting( 'graphiql_enabled' ) !== 'off';
$this->build_assets_available = $this->check_build_assets();
/**
* If GraphiQL is disabled, don't set it up in the Admin
*/
if ( ! $this->is_enabled ) {
return;
}
// Register the admin page
add_action( 'admin_menu', [ $this, 'register_admin_page' ], 9 );
add_action( 'admin_bar_menu', [ $this, 'register_admin_bar_menu' ], 100 );
// Only enqueue assets if they exist - the render method handles showing
// a helpful message when assets are missing
if ( ! $this->build_assets_available ) {
return;
}
// Enqueue GraphiQL React App
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_graphiql' ] );
// Enqueue built-in extensions via the extension action
add_action( 'enqueue_graphiql_extension', [ $this, 'enqueue_builtin_extensions' ] );
}
/**
* Check if the core JavaScript build assets are available.
*
* The /build directory is gitignored because compiled assets don't belong in
* version control. This means users who install WPGraphQL via:
* - Git clone
* - Composer (from GitHub)
*
* ...won't have the /build directory and need to run `npm ci && npm run build`.
*
* Users who install via WordPress.org or download a GitHub release ZIP get
* pre-built assets because the release workflow runs `npm run build` before
* packaging.
*
* This check prevents fatal errors from trying to include non-existent asset
* files and allows us to show a helpful message instead.
*
* @return bool True if all required build assets exist, false otherwise.
*/
protected function check_build_assets(): bool {
foreach ( self::CORE_ASSETS as $config ) {
$base_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $config['file'];
// Check required files: asset manifest and JS file
if ( ! file_exists( $base_path . '.asset.php' ) || ! file_exists( $base_path . '.js' ) ) {
return false;
}
// If asset has a style, check for CSS file too
if ( $config['has_style'] && ! file_exists( $base_path . '.css' ) ) {
return false;
}
}
return true;
}
/**
* Registers admin bar menu
*
* @param \WP_Admin_Bar $admin_bar The Admin Bar Instance
*
* @return void
*/
public function register_admin_bar_menu( WP_Admin_Bar $admin_bar ) {
if ( 'off' === get_graphql_setting( 'graphiql_enabled' ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( 'off' === get_graphql_setting( 'show_graphiql_link_in_admin_bar' ) ) {
return;
}
$icon_url = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgNDAwIj48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNNTcuNDY4IDMwMi42NmwtMTQuMzc2LTguMyAxNjAuMTUtMjc3LjM4IDE0LjM3NiA4LjN6Ii8+PHBhdGggZmlsbD0iI0UxMDA5OCIgZD0iTTM5LjggMjcyLjJoMzIwLjN2MTYuNkgzOS44eiIvPjxwYXRoIGZpbGw9IiNFMTAwOTgiIGQ9Ik0yMDYuMzQ4IDM3NC4wMjZsLTE2MC4yMS05Mi41IDguMy0xNC4zNzYgMTYwLjIxIDkyLjV6TTM0NS41MjIgMTMyLjk0N2wtMTYwLjIxLTkyLjUgOC4zLTE0LjM3NiAxNjAuMjEgOTIuNXoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNNTQuNDgyIDEzMi44ODNsLTguMy0xNC4zNzUgMTYwLjIxLTkyLjUgOC4zIDE0LjM3NnoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMzQyLjU2OCAzMDIuNjYzbC0xNjAuMTUtMjc3LjM4IDE0LjM3Ni04LjMgMTYwLjE1IDI3Ny4zOHpNNTIuNSAxMDcuNWgxNi42djE4NUg1Mi41ek0zMzAuOSAxMDcuNWgxNi42djE4NWgtMTYuNnoiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMjAzLjUyMiAzNjdsLTcuMjUtMTIuNTU4IDEzOS4zNC04MC40NSA3LjI1IDEyLjU1N3oiLz48cGF0aCBmaWxsPSIjRTEwMDk4IiBkPSJNMzY5LjUgMjk3LjljLTkuNiAxNi43LTMxIDIyLjQtNDcuNyAxMi44LTE2LjctOS42LTIyLjQtMzEtMTIuOC00Ny43IDkuNi0xNi43IDMxLTIyLjQgNDcuNy0xMi44IDE2LjggOS43IDIyLjUgMzEgMTIuOCA0Ny43TTkwLjkgMTM3Yy05LjYgMTYuNy0zMSAyMi40LTQ3LjcgMTIuOC0xNi43LTkuNi0yMi40LTMxLTEyLjgtNDcuNyA5LjYtMTYuNyAzMS0yMi40IDQ3LjctMTIuOCAxNi43IDkuNyAyMi40IDMxIDEyLjggNDcuN00zMC41IDI5Ny45Yy05LjYtMTYuNy0zLjktMzggMTIuOC00Ny43IDE2LjctOS42IDM4LTMuOSA0Ny43IDEyLjggOS42IDE2LjcgMy45IDM4LTEyLjggNDcuNy0xNi44IDkuNi0zOC4xIDMuOS00Ny43LTEyLjhNMzA5LjEgMTM3Yy05LjYtMTYuNy0zLjktMzggMTIuOC00Ny43IDE2LjctOS42IDM4LTMuOSA0Ny43IDEyLjggOS42IDE2LjcgMy45IDM4LTEyLjggNDcuNy0xNi43IDkuNi0zOC4xIDMuOS00Ny43LTEyLjhNMjAwIDM5NS44Yy0xOS4zIDAtMzQuOS0xNS42LTM0LjktMzQuOSAwLTE5LjMgMTUuNi0zNC45IDM0LjktMzQuOSAxOS4zIDAgMzQuOSAxNS42IDM0LjkgMzQuOSAwIDE5LjItMTUuNiAzNC45LTM0LjkgMzQuOU0yMDAgNzRjLTE5LjMgMC0zNC45LTE1LjYtMzQuOS0zNC45IDAtMTkuMyAxNS42LTM0LjkgMzQuOS0zNC45IDE5LjMgMCAzNC45IDE1LjYgMzQuOSAzNC45IDAgMTkuMy0xNS42IDM0LjktMzQuOSAzNC45Ii8+PC9zdmc+';
$icon = sprintf(
'<span class="custom-icon" style="
background-image:url(\'%s\'); float:left; width:22px !important; height:22px !important;
margin-left: 5px !important; margin-top: 5px !important; margin-right: 5px !important;
"></span>',
$icon_url
);
$admin_bar->add_menu(
[
'id' => 'graphiql-ide',
'title' => $icon . __( 'GraphiQL IDE', 'wp-graphql' ),
'href' => trailingslashit( admin_url() ) . 'admin.php?page=graphiql-ide',
]
);
}
/**
* Register the admin page as a subpage
*
* @return void
*/
public function register_admin_page() {
$svg_file = file_get_contents( WPGRAPHQL_PLUGIN_DIR . '/src/assets/wpgraphql-elephant.svg' );
if ( false === $svg_file ) {
return;
}
$svg_base64 = base64_encode( $svg_file );
// Top level menu page should be labeled GraphQL
add_menu_page(
__( 'GraphQL', 'wp-graphql' ),
__( 'GraphQL', 'wp-graphql' ),
'manage_options',
'graphiql-ide',
[ $this, 'render_graphiql_admin_page' ],
'data:image/svg+xml;base64,' . $svg_base64
);
// Sub menu should be labeled GraphiQL IDE
add_submenu_page(
'graphiql-ide',
__( 'GraphiQL IDE', 'wp-graphql' ),
__( 'GraphiQL IDE', 'wp-graphql' ),
'manage_options',
'graphiql-ide',
[ $this, 'render_graphiql_admin_page' ]
);
}
/**
* Render the markup for the GraphiQL admin page.
*
* When build assets are available, this outputs a container div that the
* React app will mount to. The "Loading..." text is shown briefly while
* JavaScript initializes.
*
* When build assets are missing, we show helpful instructions instead of
* a broken interface, guiding developers to either build the assets or
* download a pre-built release.
*
* @return void
*/
public function render_graphiql_admin_page() {
// If build assets are missing, show instructions instead of the loading message
if ( ! $this->build_assets_available ) {
$message = sprintf(
/* translators: 1: npm ci command, 2: npm run build command, 3: documentation URL */
__( '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' ),
'<code>npm ci</code>',
'<code>npm run build</code>',
'https://github.com/wp-graphql/wp-graphql/releases'
);
$rendered = '<div class="wrap"><div class="notice notice-warning inline"><p>' . $message . '</p></div></div>';
} else {
$rendered = apply_filters( 'graphql_render_admin_page', '<div class="wrap" dir="ltr"><div id="graphiql" class="graphiql-container">Loading ...</div></div>' );
}
echo wp_kses_post( $rendered );
}
/**
* Enqueue the core scripts and styles for the WPGraphiQL app.
*
* This loads the foundational GraphiQL interface. The core assets provide:
* - wp-graphiql: The base library exposing GraphQL utilities and hooks
* - wp-graphiql-app: The React application that renders the IDE
*
* After core assets are loaded, we fire 'enqueue_graphiql_extension' to allow
* built-in and third-party extensions to add their functionality.
*
* @return void
*/
public function enqueue_graphiql() {
if ( null === get_current_screen() || ! strpos( get_current_screen()->id, 'graphiql' ) ) {
return;
}
// Enqueue core assets
foreach ( self::CORE_ASSETS as $handle => $config ) {
$this->enqueue_asset( $handle, $config );
}
// Localize the main script with settings
wp_localize_script(
'wp-graphiql',
'wpGraphiQLSettings',
[
'nonce' => wp_create_nonce( 'wp_rest' ),
'graphqlEndpoint' => trailingslashit( site_url() ) . 'index.php?' . graphql_get_endpoint(),
'avatarUrl' => 0 !== get_current_user_id() ? get_avatar_url( get_current_user_id() ) : null,
'externalFragments' => apply_filters( 'graphiql_external_fragments', [] ),
]
);
// Extensions looking to extend GraphiQL can hook in here,
// after the window object is established, but before the App renders
do_action( 'enqueue_graphiql_extension' );
}
/**
* Enqueue the built-in GraphiQL extensions.
*
* These extensions ship with WPGraphQL but are loaded through the same
* 'enqueue_graphiql_extension' hook that third-party extensions use. This
* demonstrates the extension API and ensures our built-in features don't
* have any special privileges over community extensions.
*
* Extensions are loaded with graceful degradation - if an extension's build
* files are missing (e.g., partial build), we skip it rather than breaking
* the entire GraphiQL interface.
*
* @return void
*/
public function enqueue_builtin_extensions() {
foreach ( self::EXTENSION_ASSETS as $handle => $config ) {
// Skip extensions whose assets don't exist (graceful degradation)
$asset_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $config['file'] . '.asset.php';
if ( ! file_exists( $asset_path ) ) {
continue;
}
$this->enqueue_asset( $handle, $config );
}
}
/**
* Enqueue a single asset (script and optionally style) based on configuration.
*
* This helper centralizes the asset enqueueing logic to avoid repetition.
* It handles:
* - Loading the webpack-generated .asset.php manifest for dependencies/version
* - Merging manifest dependencies with our explicit dependencies
* - Enqueueing both JS and CSS (when applicable) with consistent patterns
*
* The .asset.php files are generated by @wordpress/scripts during `npm run build`
* and contain the list of wp-* dependencies the script needs, plus a content
* hash for cache busting.
*
* @param string $handle The WordPress script/style handle.
* @param array<string,mixed> $config The asset configuration from CORE_ASSETS or EXTENSION_ASSETS.
*/
protected function enqueue_asset( string $handle, array $config ): void {
$file = $config['file'];
$asset_path = WPGRAPHQL_PLUGIN_DIR . 'build/' . $file . '.asset.php';
// Asset file must exist
if ( ! file_exists( $asset_path ) ) {
return;
}
// phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Path is constructed from WPGRAPHQL_PLUGIN_DIR constant + hardcoded string, validated with file_exists()
$asset_file = include $asset_path;
// Enqueue the script
wp_enqueue_script(
$handle,
WPGRAPHQL_PLUGIN_URL . 'build/' . $file . '.js',
array_merge( $config['script_deps'], $asset_file['dependencies'] ),
$asset_file['version'],
true
);
// Set up script translations for i18n
wp_set_script_translations( $handle, 'wp-graphql', WPGRAPHQL_PLUGIN_DIR . 'languages' );
// Enqueue the style if this asset has one
if ( ! empty( $config['has_style'] ) ) {
wp_enqueue_style(
$handle,
WPGRAPHQL_PLUGIN_URL . 'build/' . $file . '.css',
$config['style_deps'],
$asset_file['version']
);
}
}
}