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/Mutation/MediaItemCreate.php
<?php

namespace WPGraphQL\Mutation;

use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\MediaItemMutation;
use WPGraphQL\Utils\Utils;

class MediaItemCreate {
	/**
	 * Registers the MediaItemCreate mutation.
	 *
	 * @return void
	 * @throws \Exception
	 */
	public static function register_mutation() {
		register_graphql_mutation(
			'createMediaItem',
			[
				'inputFields'         => self::get_input_fields(),
				'outputFields'        => self::get_output_fields(),
				'mutateAndGetPayload' => self::mutate_and_get_payload(),
			]
		);
	}

	/**
	 * Defines the mutation input field configuration.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public static function get_input_fields() {
		return [
			'altText'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'Alternative text to display when mediaItem is not displayed', 'wp-graphql' );
				},
			],
			'authorId'      => [
				'type'        => 'ID',
				'description' => static function () {
					return __( 'The userId to assign as the author of the mediaItem', 'wp-graphql' );
				},
			],
			'caption'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The caption for the mediaItem', 'wp-graphql' );
				},
			],
			'commentStatus' => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The comment status for the mediaItem', 'wp-graphql' );
				},
			],
			'date'          => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The date of the mediaItem', 'wp-graphql' );
				},
			],
			'dateGmt'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The date (in GMT zone) of the mediaItem', 'wp-graphql' );
				},
			],
			'description'   => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'Description of the mediaItem', 'wp-graphql' );
				},
			],
			'filePath'      => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The file name of the mediaItem', 'wp-graphql' );
				},
			],
			'fileType'      => [
				'type'        => 'MimeTypeEnum',
				'description' => static function () {
					return __( 'The file type of the mediaItem', 'wp-graphql' );
				},
			],
			'slug'          => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The slug of the mediaItem', 'wp-graphql' );
				},
			],
			'status'        => [
				'type'        => 'MediaItemStatusEnum',
				'description' => static function () {
					return __( 'The status of the mediaItem', 'wp-graphql' );
				},
			],
			'title'         => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The title of the mediaItem', 'wp-graphql' );
				},
			],
			'pingStatus'    => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The ping status for the mediaItem', 'wp-graphql' );
				},
			],
			'parentId'      => [
				'type'        => 'ID',
				'description' => static function () {
					return __( 'The ID of the parent object', 'wp-graphql' );
				},
			],
		];
	}

	/**
	 * Defines the mutation output field configuration.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public static function get_output_fields() {
		return [
			'mediaItem' => [
				'type'        => 'MediaItem',
				'description' => static function () {
					return __( 'The MediaItem object mutation type.', 'wp-graphql' );
				},
				'resolve'     => static function ( $payload, $args, AppContext $context ) {
					if ( empty( $payload['postObjectId'] ) || ! absint( $payload['postObjectId'] ) ) {
						return null;
					}

					return $context->get_loader( 'post' )->load_deferred( $payload['postObjectId'] );
				},
			],
		];
	}

	/**
	 * Defines the mutation data modification closure.
	 *
	 * @return callable(array<string,mixed>$input,\WPGraphQL\AppContext $context,\GraphQL\Type\Definition\ResolveInfo $info):array<string,mixed>
	 */
	public static function mutate_and_get_payload() {
		return static function ( $input, AppContext $context, ResolveInfo $info ) {
			/**
			 * Stop now if a user isn't allowed to upload a mediaItem
			 */
			if ( ! current_user_can( 'upload_files' ) ) {
				throw new UserError( esc_html__( 'Sorry, you are not allowed to upload mediaItems', 'wp-graphql' ) );
			}

			$post_type_object = get_post_type_object( 'attachment' );
			if ( empty( $post_type_object ) ) {
				throw new UserError( esc_html__( 'The Media Item could not be created', 'wp-graphql' ) );
			}

			/**
			 * If the mediaItem being created is being assigned to another user that's not the current user, make sure
			 * the current user has permission to edit others mediaItems
			 */
			if ( ! empty( $input['authorId'] ) ) {
				// Ensure authorId is a valid databaseId.
				$input['authorId'] = Utils::get_database_id_from_id( $input['authorId'] );

				// Bail if can't edit other users' attachments.
				if ( get_current_user_id() !== $input['authorId'] && ( ! isset( $post_type_object->cap->edit_others_posts ) || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
					throw new UserError( esc_html__( 'Sorry, you are not allowed to create mediaItems as this user', 'wp-graphql' ) );
				}
			}

			/**
			 * Set the file name, whether it's a local file or from a URL.
			 * Then set the url for the uploaded file
			 */
			$file_name           = basename( $input['filePath'] );
			$uploaded_file_url   = (string) $input['filePath'];
			$sanitized_file_path = sanitize_file_name( $input['filePath'] );

			// Check that the filetype is allowed
			$check_file = wp_check_filetype( $sanitized_file_path );

			// if the file doesn't pass the check, throw an error
			if ( ! $check_file['ext'] || ! $check_file['type'] || ! wp_http_validate_url( $uploaded_file_url ) ) {
				// translators: %s is the file path.
				throw new UserError( esc_html( sprintf( __( 'Invalid filePath "%s"', 'wp-graphql' ), $input['filePath'] ) ) );
			}

			$protocol = wp_parse_url( $input['filePath'], PHP_URL_SCHEME );

			// prevent the filePath from being submitted with a non-allowed protocols
			$allowed_protocols = [ 'https', 'http', 'file' ];

			/**
			 * Filter the allowed protocols for the mutation
			 *
			 * @param string[]                             $allowed_protocols The allowed protocols for filePaths to be submitted
			 * @param mixed                                $protocol          The current protocol of the filePath
			 * @param array<string,mixed>                  $input             The input of the current mutation
			 * @param \WPGraphQL\AppContext                $context           The context of the current request
			 * @param \GraphQL\Type\Definition\ResolveInfo $info              The ResolveInfo of the current field
			 */
			$allowed_protocols = apply_filters( 'graphql_media_item_create_allowed_protocols', $allowed_protocols, $protocol, $input, $context, $info );

			if ( ! in_array( $protocol, $allowed_protocols, true ) ) {
				throw new UserError(
					esc_html(
						sprintf(
							// translators: %1$s is the protocol, %2$s is the list of allowed protocols.
							__( 'Invalid protocol. "%1$s". Only "%2$s" allowed.', 'wp-graphql' ),
							$protocol,
							implode( '", "', $allowed_protocols )
						)
					)
				);
			}

			/**
			 * Require the file.php file from wp-admin. This file includes the
			 * download_url and wp_handle_sideload methods.
			 */
			require_once ABSPATH . 'wp-admin/includes/file.php';

			$file_contents = file_get_contents( $input['filePath'] );

			/**
			 * If the mediaItem file is from a local server, use wp_upload_bits before saving it to the uploads folder
			 */
			if ( 'file' === wp_parse_url( $input['filePath'], PHP_URL_SCHEME ) && ! empty( $file_contents ) ) {
				$uploaded_file     = wp_upload_bits( $file_name, null, $file_contents );
				$uploaded_file_url = ( empty( $uploaded_file['error'] ) ? $uploaded_file['url'] : null );
			}

			/**
			 * Ensure we have a valid URL before attempting download
			 */
			if ( empty( $uploaded_file_url ) ) {
				throw new UserError( esc_html__( 'Sorry, the file could not be uploaded', 'wp-graphql' ) );
			}

			/**
			 * URL data for the mediaItem, timeout value is the default, see:
			 * https://developer.wordpress.org/reference/functions/download_url/
			 */
			$timeout_seconds = 300;

			$temp_file = download_url( $uploaded_file_url, $timeout_seconds );

			/**
			 * Handle the error from download_url if it occurs
			 */
			if ( is_wp_error( $temp_file ) ) {
				throw new UserError( esc_html__( 'Sorry, the URL for this file is invalid, it must be a valid URL', 'wp-graphql' ) );
			}

			/**
			 * Build the file data for side loading
			 */
			$file_data = [
				'name'     => $file_name,
				'type'     => ! empty( $input['fileType'] ) ? $input['fileType'] : wp_check_filetype( $temp_file ),
				'tmp_name' => $temp_file,
				'error'    => 0,
				'size'     => (int) filesize( $temp_file ),
			];

			/**
			 * Tells WordPress to not look for the POST form fields that would normally be present as
			 * we downloaded the file from a remote server, so there will be no form fields
			 * The default is true
			 */
			$overrides = [
				'test_form' => false,
			];

			/**
			 * Insert the mediaItem and retrieve it's data
			 */
			$file = wp_handle_sideload( $file_data, $overrides );

			/**
			 * Handle the error from wp_handle_sideload if it occurs
			 */
			if ( ! empty( $file['error'] ) ) {
				throw new UserError( esc_html__( 'Sorry, the URL for this file is invalid, it must be a path to the mediaItem file', 'wp-graphql' ) );
			}

			/**
			 * Insert the mediaItem object and get the ID
			 */
			$media_item_args = MediaItemMutation::prepare_media_item( $input, $post_type_object, 'createMediaItem', $file );

			/**
			 * Get the post parent and if it's not set, set it to 0
			 */
			$attachment_parent_id = ! empty( $media_item_args['post_parent'] ) ? $media_item_args['post_parent'] : 0;

			/**
			 * Stop now if a user isn't allowed to edit the parent post
			 */
			$parent = get_post( $attachment_parent_id );

			if ( null !== $parent ) {
				$post_parent_type = get_post_type_object( $parent->post_type );

				if ( empty( $post_parent_type ) ) {
					throw new UserError( esc_html__( 'The parent of the Media Item is of an invalid type', 'wp-graphql' ) );
				}

				if ( 'attachment' !== $post_parent_type->name && ( ! isset( $post_parent_type->cap->edit_post ) || ! current_user_can( $post_parent_type->cap->edit_post, $attachment_parent_id ) ) ) {
					throw new UserError( esc_html__( 'Sorry, you are not allowed to upload mediaItems assigned to this parent node', 'wp-graphql' ) );
				}
			}

			/**
			 * Insert the mediaItem
			 *
			 * Required Argument defaults are set in the main MediaItemMutation.php if they aren't set
			 * by the user during input, they are:
			 * post_title (pulled from file if not entered)
			 * post_content (empty string if not entered)
			 * post_status (inherit if not entered)
			 * post_mime_type (pulled from the file if not entered in the mutation)
			 */
			$attachment_id = wp_insert_attachment( $media_item_args, $file['file'], $attachment_parent_id, true );

			if ( is_wp_error( $attachment_id ) ) {
				$error_message = $attachment_id->get_error_message();
				if ( ! empty( $error_message ) ) {
					throw new UserError( esc_html( $error_message ) );
				}

				throw new UserError( esc_html__( 'The media item failed to create but no error was provided', 'wp-graphql' ) );
			}

			/**
			 * Check if the wp_generate_attachment_metadata method exists and include it if not.
			 */
			require_once ABSPATH . 'wp-admin/includes/image.php';

			/**
			 * Generate and update the mediaItem's metadata.
			 * If we make it this far the file and attachment
			 * have been validated and we will not receive any errors
			 */
			$attachment_data = wp_generate_attachment_metadata( $attachment_id, $file['file'] );
			wp_update_attachment_metadata( $attachment_id, $attachment_data );

			/**
			 * Update alt text postmeta for mediaItem
			 */
			MediaItemMutation::update_additional_media_item_data( $attachment_id, $input, $post_type_object, 'createMediaItem', $context, $info );

			return [
				'postObjectId' => $attachment_id,
			];
		};
	}
}