HEX
Server: Apache/2.4.66 (Debian)
System: Linux 6dfabc3b2241 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64
User: (1000)
PHP: 8.3.30
Disabled: NONE
Upload Files
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']
			);
		}
	}
}