<?php
/**
 * Model - PostObject
 *
 * @package WPGraphQL\Model
 */

namespace WPGraphQL\Model;

use GraphQLRelay\Relay;
use WPGraphQL\Utils\Utils;
use WP_Post;

/**
 * Class Post - Models data for the Post object type
 *
 * @property ?int          $authorDatabaseId
 * @property ?string       $authorId
 * @property int           $commentCount
 * @property ?string       $commentStatus
 * @property ?string       $contentRaw
 * @property ?string       $contentRendered
 * @property ?int          $databaseId
 * @property ?string       $date
 * @property ?string       $dateGmt
 * @property ?int          $editLastId
 * @property string[]|null $editLock
 * @property ?string       $enclosure
 * @property ?string       $excerptRaw
 * @property ?string       $excerptRendered
 * @property ?int          $featuredImageDatabaseId
 * @property ?string       $featuredImageId
 * @property ?string       $guid
 * @property bool          $hasPassword
 * @property ?string       $id
 * @property bool          $isFrontPage
 * @property bool          $isPostsPage
 * @property bool          $isPreview
 * @property bool          $isPrivacyPage
 * @property bool          $isRevision
 * @property bool          $isSticky
 * @property ?string       $link
 * @property ?int          $menuOrder
 * @property ?string       $modified
 * @property ?string       $modifiedGmt
 * @property ?string       $pageTemplate
 * @property ?int          $parentDatabaseId
 * @property ?string       $parentId
 * @property ?string       $password
 * @property ?string       $pinged
 * @property ?string       $pingStatus
 * @property ?string       $post_type
 * @property int           $previewRevisionDatabaseId
 * @property ?string       $slug
 * @property ?string       $status
 * @property array{
 *  __typename: string,
 *  templateName: string
 * }                       $template
 * @property ?string       $titleRaw
 * @property ?string       $titleRendered
 * @property ?string       $toPing
 * @property ?string       $uri
 *
 * Attachment specific fields:
 * @property string|null              $altText
 * @property string|null              $captionRaw
 * @property string|null              $captionRendered
 * @property string|null              $descriptionRaw
 * @property string|null              $descriptionRendered
 * @property array<string,mixed>|null $mediaDetails
 * @property string|null              $mediaItemUrl
 * @property string|null              $mediaType
 * @property string|null              $mimeType
 * @property string|null              $sourceUrl
 *
 * Aliases:
 * @property ?int    $ID
 * @property ?int    $post_author
 * @property ?string $post_status
 *
 * @extends \WPGraphQL\Model\Model<\WP_Post>
 */
class Post extends Model {
	/**
	 * Store the global post to reset during model tear down
	 *
	 * @var \WP_Post
	 */
	protected $global_post;

	/**
	 * Stores the incoming post type object for the post being modeled
	 *
	 * @var \WP_Post_Type|null $post_type_object
	 */
	protected $post_type_object;

	/**
	 * Store the instance of the WP_Query
	 *
	 * @var \WP_Query
	 */
	protected $wp_query;

	/**
	 * Stores the resolved image `sourceUrl`s keyed by size.
	 *
	 * This is used to prevent multiple calls to `wp_get_attachment_image_src`.
	 *
	 * If no source URL is found for a size, the value will be `null`.
	 *
	 * @var array<string,?string>
	 */
	protected $source_urls_by_size = [];

	/**
	 * Post constructor.
	 *
	 * @param \WP_Post $post The incoming WP_Post object that needs modeling.
	 *
	 * @return void
	 */
	public function __construct( WP_Post $post ) {

		/**
		 * Set the data as the Post object
		 */
		$this->data             = $post;
		$this->post_type_object = get_post_type_object( $post->post_type );

		/**
		 * If the post type is 'revision', we need to get the post_type_object
		 * of the parent post type to determine capabilities from
		 */
		if ( 'revision' === $post->post_type && ! empty( $post->post_parent ) ) {
			$parent = get_post( absint( $post->post_parent ) );
			if ( ! empty( $parent ) ) {
				$this->post_type_object = get_post_type_object( $parent->post_type );
			}
		}

		/**
		 * Mimic core functionality for templates, as seen here:
		 * https://github.com/WordPress/WordPress/blob/6fd8080e7ee7599b36d4528f72a8ced612130b8c/wp-includes/template-loader.php#L56
		 */
		if ( 'attachment' === $this->data->post_type ) {
			remove_filter( 'the_content', 'prepend_attachment' );
		}

		$allowed_restricted_fields = [
			'databaseId',
			'enqueuedScriptsQueue',
			'enqueuedStylesheetsQueue',
			'hasPassword',
			'id',
			'isFrontPage',
			'isPostsPage',
			'isPrivacyPage',
			'isRestricted',
			'link',
			'post_status',
			'post_type',
			'slug',
			'status',
			'titleRendered',
			'uri',
		];

		if ( isset( $this->post_type_object->graphql_single_name ) ) {
			$allowed_restricted_fields[] = $this->post_type_object->graphql_single_name . 'Id';
		}

		$restricted_cap = $this->get_restricted_cap();

		parent::__construct( $restricted_cap, $allowed_restricted_fields, (int) $post->post_author );
	}

	/**
	 * {@inheritDoc}
	 */
	public function setup() {
		global $wp_query, $post;

		/**
		 * Store the global post before overriding
		 */
		$this->global_post = $post;

		/**
		 * Set the resolving post to the global $post. That way any filters that
		 * might be applied when resolving fields can rely on global post and
		 * post data being set up.
		 */
		if ( $this->data instanceof WP_Post ) {
			$id        = $this->data->ID;
			$post_type = $this->data->post_type;
			$post_name = $this->data->post_name;
			$data      = $this->data;

			if ( 'revision' === $this->data->post_type ) {
				$id     = $this->data->post_parent;
				$parent = get_post( $this->data->post_parent );
				if ( empty( $parent ) ) {
					$this->fields = [];
					return;
				}
				$post_type = $parent->post_type;
				$post_name = $parent->post_name;
				$data      = $parent;
			}

			/**
			 * Clear out existing postdata
			 */
			$wp_query->reset_postdata();

			/**
			 * Parse the query to tell WordPress how to
			 * setup global state
			 */
			if ( 'post' === $post_type ) {
				$wp_query->parse_query(
					[
						'page' => '',
						'p'    => $id,
					]
				);
			} elseif ( 'page' === $post_type ) {
				$wp_query->parse_query(
					[
						'page'     => '',
						'pagename' => $post_name,
					]
				);
			} elseif ( 'attachment' === $post_type ) {
				$wp_query->parse_query(
					[
						'attachment' => $post_name,
					]
				);
			} else {
				$wp_query->parse_query(
					[
						$post_type  => $post_name,
						'post_type' => $post_type,
						'name'      => $post_name,
					]
				);
			}

			$wp_query->setup_postdata( $data );
			$GLOBALS['post']             = $data; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
			$wp_query->queried_object    = get_post( $this->data->ID );
			$wp_query->queried_object_id = $this->data->ID;
		}
	}

	/**
	 * Retrieve the cap to check if the data should be restricted for the post
	 *
	 * @return string
	 */
	protected function get_restricted_cap() {
		if ( ! empty( $this->data->post_password ) ) {
			return isset( $this->post_type_object->cap->edit_others_posts ) ? $this->post_type_object->cap->edit_others_posts : 'edit_others_posts';
		}

		switch ( $this->data->post_status ) {
			case 'trash':
				$cap = isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts';
				break;
			case 'draft':
			case 'future':
			case 'pending':
				$cap = isset( $this->post_type_object->cap->edit_others_posts ) ? $this->post_type_object->cap->edit_others_posts : 'edit_others_posts';
				break;
			default:
				$cap = '';
				break;
		}

		return $cap;
	}

	/**
	 * {@inheritDoc}
	 */
	public function is_private() {

		/**
		 * If the post is of post_type "revision", we need to access the parent of the Post
		 * so that we can check access rights of the parent post. Revision access is inherit
		 * to the Parent it is a revision of.
		 */
		if ( 'revision' === $this->data->post_type ) {

			// Get the post
			$parent_post = get_post( $this->data->post_parent );

			// If the parent post doesn't exist, the revision should be considered private
			if ( ! $parent_post instanceof WP_Post ) {
				return true;
			}

			// Determine if the revision is private using capabilities relative to the parent
			return $this->is_post_private( $parent_post );
		}

		/**
		 * Media Items (attachments) with "inherit" status should inherit their privacy
		 * from their parent post. If the parent is draft/pending/private, the attachment
		 * should also be considered private.
		 *
		 * Attachments without a parent or with a published parent are public. Once uploaded
		 * to the media library they are exposed with a public URL on the site.
		 *
		 * The WP REST API sets media items to private if they don't have a `post_parent` set, but
		 * this has broken production apps, because media items can be uploaded directly to the
		 * media library and published as a featured image, published inline within content, or
		 * within a Gutenberg block, etc, but then a consumer tries to ask for data of a published
		 * image and REST returns nothing because the media item is treated as private.
		 *
		 * For attachments with "inherit" status, we check the parent's privacy status.
		 * For other attachments, they are treated as public.
		 *
		 * To override this behavior and make all media items public (regardless of parent status),
		 * use the `graphql_pre_model_data_is_private` filter:
		 *
		 * @example
		 * ```php
		 * add_filter( 'graphql_pre_model_data_is_private', function( $is_private, $model_name, $data ) {
		 *     // Make all media items public
		 *     if ( 'PostObject' === $model_name && isset( $data->post_type ) && 'attachment' === $data->post_type ) {
		 *         return false; // false = not private (public)
		 *     }
		 *     return $is_private; // Return null to use default logic for other types
		 * }, 10, 3 );
		 * ```
		 */
		if ( 'attachment' === $this->data->post_type ) {
			// If the attachment has "inherit" status and a parent post, check the parent's privacy
			// This ensures attachments inherit visibility from their parent (e.g., draft post = private attachment)
			if ( 'inherit' === $this->data->post_status && ! empty( $this->data->post_parent ) ) {
				$parent_post = get_post( (int) $this->data->post_parent );

				// If parent doesn't exist (e.g., was deleted), treat as public
				// This aligns with the documented reasoning: attachments without parents are public
				// because they may be used in published content, featured images, etc.
				if ( ! $parent_post instanceof WP_Post ) {
					return false;
				}

				// Check if the parent post would be private
				// Create a temporary Post model to check the parent's privacy
				// This properly initializes the parent's post type object and checks its privacy status
				$parent_model = new self( $parent_post );
				return $parent_model->is_private();
			}

			// Attachments without inherit status or without a parent are public
			// This preserves the documented behavior: media items uploaded directly to the library
			// or used in published content should be publicly accessible
			return false;
		}

		/**
		 * Published content is public, not private
		 */
		if ( 'publish' === $this->data->post_status && $this->post_type_object && ( true === $this->post_type_object->public || true === $this->post_type_object->publicly_queryable ) ) {
			return false;
		}

		return $this->is_post_private( $this->data );
	}

	/**
	 * Method for determining if the data should be considered private or not
	 *
	 * @param \WP_Post $post_object The object of the post we need to verify permissions for
	 *
	 * @return bool
	 */
	protected function is_post_private( $post_object = null ) {
		$post_type_object = $this->post_type_object;

		if ( ! $post_type_object ) {
			return true;
		}

		if ( ! $post_object ) {
			$post_object = $this->data;
		}

		/**
		 * If the status is NOT publish and the user does NOT have capabilities to edit posts,
		 * consider the post private.
		 */
		if ( ! isset( $post_type_object->cap->edit_posts ) || ! current_user_can( $post_type_object->cap->edit_posts ) ) {
			return true;
		}

		/**
		 * If the owner of the content is the current user
		 */
		if ( ( true === $this->owner_matches_current_user() ) && 'revision' !== $post_object->post_type ) {
			return false;
		}

		/**
		 * If the post_type isn't (not registered) or is not allowed in WPGraphQL,
		 * mark the post as private
		 */

		if ( empty( $post_type_object->name ) || ! in_array( $post_type_object->name, \WPGraphQL::get_allowed_post_types(), true ) ) {
			return true;
		}

		if ( 'private' === $this->data->post_status && ( ! isset( $post_type_object->cap->read_private_posts ) || ! current_user_can( $post_type_object->cap->read_private_posts ) ) ) {
			return true;
		}

		if ( 'revision' === $this->data->post_type || 'auto-draft' === $this->data->post_status ) {
			$parent = get_post( (int) $this->data->post_parent );

			if ( empty( $parent ) ) {
				return true;
			}

			$parent_post_type_obj = $post_type_object;

			if ( 'private' === $parent->post_status ) {
				$cap = isset( $parent_post_type_obj->cap->read_private_posts ) ? $parent_post_type_obj->cap->read_private_posts : 'read_private_posts';
			} else {
				$cap = isset( $parent_post_type_obj->cap->edit_post ) ? $parent_post_type_obj->cap->edit_post : 'edit_post';
			}

			if ( ! current_user_can( $cap, $parent->ID ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	protected function init() {
		if ( empty( $this->fields ) ) {
			$this->fields = [
				'authorDatabaseId'          => function () {
					if ( true === $this->isPreview ) {
						$parent_post = get_post( $this->data->post_parent );

						return ! empty( $parent_post->post_author ) ? (int) $parent_post->post_author : null;
					}

					return ! empty( $this->data->post_author ) ? (int) $this->data->post_author : null;
				},
				'authorId'                  => function () {
					return ! empty( $this->authorDatabaseId ) ? Relay::toGlobalId( 'user', (string) $this->authorDatabaseId ) : null;
				},
				'commentCount'              => function () {
					return ! empty( $this->data->comment_count ) ? absint( $this->data->comment_count ) : 0;
				},
				'commentStatus'             => function () {
					return ! empty( $this->data->comment_status ) ? $this->data->comment_status : null;
				},
				'contentRaw'                => [
					'callback'   => function () {
						return ! empty( $this->data->post_content ) ? $this->data->post_content : null;
					},
					'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
				],
				'contentRendered'           => function () {
					$content = ! empty( $this->data->post_content ) ? $this->data->post_content : null;

					return ! empty( $content ) ? $this->html_entity_decode( apply_filters( 'the_content', $content ), 'contentRendered', false ) : null;
				},
				'databaseId'                => function () {
					return ! empty( $this->data->ID ) ? absint( $this->data->ID ) : null;
				},
				'date'                      => function () {
					return ! empty( $this->data->post_date ) && '0000-00-00 00:00:00' !== $this->data->post_date ? Utils::prepare_date_response( $this->data->post_date_gmt, $this->data->post_date ) : null;
				},
				'dateGmt'                   => function () {
					return ! empty( $this->data->post_date_gmt ) ? Utils::prepare_date_response( $this->data->post_date_gmt ) : null;
				},
				'editLastId'                => function () {
					$edit_last = get_post_meta( $this->data->ID, '_edit_last', true );

					return ! empty( $edit_last ) ? absint( $edit_last ) : null;
				},
				'editLock'                  => function () {
					if ( ! function_exists( 'wp_check_post_lock' ) ) {
						require_once ABSPATH . 'wp-admin/includes/post.php';
					}

					if ( ! wp_check_post_lock( $this->data->ID ) ) {
						return null;
					}

					$edit_lock       = get_post_meta( $this->data->ID, '_edit_lock', true );
					$edit_lock_parts = ! empty( $edit_lock ) ? explode( ':', $edit_lock ) : null;

					return ! empty( $edit_lock_parts ) ? $edit_lock_parts : null;
				},
				'enclosure'                 => function () {
					$enclosure = get_post_meta( $this->data->ID, 'enclosure', true );

					return ! empty( $enclosure ) ? $enclosure : null;
				},
				'enqueuedScriptsQueue'      => static function () {
					global $wp_scripts;
					do_action( 'wp_enqueue_scripts' );
					$queue = $wp_scripts->queue;
					$wp_scripts->reset();
					$wp_scripts->queue = [];

					return $queue;
				},
				'enqueuedStylesheetsQueue'  => static function () {
					global $wp_styles;
					do_action( 'wp_enqueue_scripts' );
					$queue = $wp_styles->queue;
					$wp_styles->reset();
					$wp_styles->queue = [];

					return $queue;
				},
				'excerptRaw'                => [
					'callback'   => function () {
						return ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : null;
					},
					'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
				],
				'excerptRendered'           => function () {
					$excerpt = ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : '';
					$excerpt = apply_filters( 'get_the_excerpt', $excerpt, $this->data );

					return $this->html_entity_decode( apply_filters( 'the_excerpt', $excerpt ), 'excerptRendered' );
				},
				'featuredImageDatabaseId'   => function () {
					if ( $this->isRevision ) {
						$id = $this->parentDatabaseId;
					} else {
						$id = $this->data->ID;
					}

					$thumbnail_id = get_post_thumbnail_id( $id );

					return ! empty( $thumbnail_id ) ? absint( $thumbnail_id ) : null;
				},
				'featuredImageId'           => function () {
					return ! empty( $this->featuredImageDatabaseId ) ? Relay::toGlobalId( 'post', (string) $this->featuredImageDatabaseId ) : null;
				},
				'guid'                      => function () {
					return ! empty( $this->data->guid ) ? $this->data->guid : null;
				},
				'hasPassword'               => function () {
					return ! empty( $this->data->post_password );
				},
				'id'                        => function () {
					return ! empty( $this->data->post_type && ! empty( $this->databaseId ) ) ? Relay::toGlobalId( 'post', (string) $this->databaseId ) : null;
				},
				'isFrontPage'               => function () {
					if ( 'page' !== $this->data->post_type || 'page' !== get_option( 'show_on_front' ) ) {
						return false;
					}
					if ( absint( get_option( 'page_on_front', 0 ) ) === $this->data->ID ) {
						return true;
					}

					return false;
				},
				'isPostsPage'               => function () {
					if ( 'page' !== $this->data->post_type ) {
						return false;
					}
					if ( 'posts' !== get_option( 'show_on_front', 'posts' ) && absint( get_option( 'page_for_posts', 0 ) ) === $this->data->ID ) {
						return true;
					}

					return false;
				},
				'isPreview'                 => function () {
					if ( $this->isRevision ) {
						$revisions = wp_get_post_revisions(
							$this->data->post_parent,
							[
								'posts_per_page' => 1,
								'fields'         => 'ids',
								'check_enabled'  => false,
							]
						);

						if ( in_array( $this->data->ID, array_values( $revisions ), true ) ) {
							return true;
						}
					}

					if ( ! post_type_supports( $this->data->post_type, 'revisions' ) && 'draft' === $this->data->post_status ) {
						return true;
					}

					return false;
				},
				'isPrivacyPage'             => function () {
					if ( 'page' !== $this->data->post_type ) {
						return false;
					}
					if ( absint( get_option( 'wp_page_for_privacy_policy', 0 ) ) === $this->data->ID ) {
						return true;
					}

					return false;
				},
				'isRevision'                => function () {
					return 'revision' === $this->data->post_type;
				},
				'isSticky'                  => function () {
					return is_sticky( $this->data->ID );
				},
				'link'                      => function () {
					$link = get_permalink( $this->data->ID );

					if ( $this->isPreview ) {
						$link = get_preview_post_link( $this->parentDatabaseId );
					} elseif ( $this->isRevision ) {
						$link = get_permalink( $this->data->ID );
					}

					return ! empty( $link ) ? urldecode( $link ) : null;
				},
				'menuOrder'                 => function () {
					return ! empty( $this->data->menu_order ) ? absint( $this->data->menu_order ) : null;
				},
				'modified'                  => function () {
					return ! empty( $this->data->post_modified ) && '0000-00-00 00:00:00' !== $this->data->post_modified ? Utils::prepare_date_response( $this->data->post_modified ) : null;
				},
				'modifiedGmt'               => function () {
					return ! empty( $this->data->post_modified_gmt ) ? Utils::prepare_date_response( $this->data->post_modified_gmt ) : null;
				},
				'pageTemplate'              => function () {
					$slug = get_page_template_slug( $this->data->ID );

					return ! empty( $slug ) ? $slug : null;
				},
				'parentDatabaseId'          => function () {
					return ! empty( $this->data->post_parent ) ? absint( $this->data->post_parent ) : null;
				},
				'parentId'                  => function () {
					return ( ! empty( $this->data->post_type ) && ! empty( $this->parentDatabaseId ) ) ? Relay::toGlobalId( 'post', (string) $this->parentDatabaseId ) : null;
				},
				'password'                  => function () {
					return ! empty( $this->data->post_password ) ? $this->data->post_password : null;
				},
				'pinged'                    => function () {
					$punged = get_pung( $this->data->ID );

					return empty( $punged ) ? null : implode( ',', (array) $punged );
				},
				'pingStatus'                => function () {
					return ! empty( $this->data->ping_status ) ? $this->data->ping_status : null;
				},
				'post_type'                 => function () {
					return ! empty( $this->data->post_type ) ? $this->data->post_type : null;
				},
				'previewRevisionDatabaseId' => [
					'callback'   => function () {
						$revisions = wp_get_post_revisions(
							$this->data->ID,
							[
								'posts_per_page' => 1,
								'fields'         => 'ids',
								'check_enabled'  => false,
							]
						);

						return is_array( $revisions ) && ! empty( $revisions ) ? array_values( $revisions )[0] : null;
					},
					'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
				],
				'previewRevisionId'         => function () {
					return ! empty( $this->previewRevisionDatabaseId ) ? Relay::toGlobalId( 'post', (string) $this->previewRevisionDatabaseId ) : null;
				},
				'slug'                      => function () {
					return ! empty( $this->data->post_name ) ? urldecode( $this->data->post_name ) : null;
				},
				'status'                    => function () {
					return ! empty( $this->data->post_status ) ? $this->data->post_status : null;
				},
				'template'                  => function () {
					$registered_templates = wp_get_theme()->get_page_templates( null, $this->data->post_type );

					$template = [
						'__typename'   => 'DefaultTemplate',
						'templateName' => 'Default',
					];

					if ( true === $this->isPreview ) {
						$parent_post = get_post( $this->parentDatabaseId );

						if ( empty( $parent_post ) ) {
							return $template;
						}

						/** @var \WP_Post $parent_post */
						$registered_templates = wp_get_theme()->get_page_templates( $parent_post );

						if ( empty( $registered_templates ) ) {
							return $template;
						}
						$set_template  = get_post_meta( $parent_post->ID, '_wp_page_template', true );
						$template_name = get_page_template_slug( $parent_post->ID );

						if ( empty( $set_template ) ) {
							$set_template = get_post_meta( $this->data->ID, '_wp_page_template', true );
						}

						if ( empty( $template_name ) ) {
							$template_name = get_page_template_slug( $this->data->ID );
						}

						$template_name = ! empty( $template_name ) ? $template_name : 'Default';
					} else {
						if ( empty( $registered_templates ) ) {
							return $template;
						}

						$set_template  = get_post_meta( $this->data->ID, '_wp_page_template', true );
						$template_name = get_page_template_slug( $this->data->ID );

						$template_name = ! empty( $template_name ) ? $template_name : 'Default';
					}

					if ( ! empty( $registered_templates[ $set_template ] ) ) {
						$name = Utils::format_type_name_for_wp_template( $registered_templates[ $set_template ], $set_template );

						// If the name is empty, fallback to DefaultTemplate
						if ( empty( $name ) ) {
							$name = 'DefaultTemplate';
						}

						$template = [
							'__typename'   => $name,
							'templateName' => ucwords( $registered_templates[ $set_template ] ),
						];
					}

					return $template;
				},
				'titleRaw'                  => [
					'callback'   => function () {
						return ! empty( $this->data->post_title ) ? $this->data->post_title : null;
					},
					'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
				],
				'titleRendered'             => function () {
					$id    = ! empty( $this->data->ID ) ? $this->data->ID : null;
					$title = ! empty( $this->data->post_title ) ? $this->data->post_title : '';

					$processedTitle = ! empty( $title ) ? $this->html_entity_decode( apply_filters( 'the_title', $title, $id ), 'titleRendered', true ) : '';

					return empty( $processedTitle ) ? null : $processedTitle;
				},
				'toPing'                    => function () {
					$to_ping = get_to_ping( $this->data->ID );

					return ! empty( $to_ping ) ? implode( ',', (array) $to_ping ) : null;
				},
				'uri'                       => function () {
					$uri = $this->link;

					if ( true === $this->isFrontPage ) {
						return '/';
					}

					// if the page is set as the posts page
					// the page node itself is not identifiable
					// by URI. Instead, the uri would return the
					// Post content type as that uri
					// represents the blog archive instead of a page
					if ( true === $this->isPostsPage ) {
						return null;
					}

					return ! empty( $uri ) ? str_ireplace( home_url(), '', $uri ) : null;
				},

				// Aliases.
				'ID'                        => function () {
					return $this->databaseId;
				},
				'post_author'               => function () {
					return $this->authorDatabaseId;
				},
				'post_status'               => function () {
					return $this->status;
				},
			];

			if ( 'attachment' === $this->data->post_type ) {
				$attachment_fields = [
					'altText'             => function () {
						return get_post_meta( $this->data->ID, '_wp_attachment_image_alt', true );
					},
					'captionRaw'          => [
						'callback'   => function () {
							return ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : null;
						},
						'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
					],
					'captionRendered'     => function () {
						$caption = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $this->data->post_excerpt, $this->data ) );

						return ! empty( $caption ) ? $caption : null;
					},
					'descriptionRaw'      => [
						'callback'   => function () {
							return ! empty( $this->data->post_content ) ? $this->data->post_content : null;
						},
						'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
					],
					'descriptionRendered' => function () {
						return ! empty( $this->data->post_content ) ? apply_filters( 'the_content', $this->data->post_content ) : null;
					},
					'mediaDetails'        => function () {
						$media_details = wp_get_attachment_metadata( $this->data->ID );
						if ( ! empty( $media_details ) ) {
							$media_details['ID'] = $this->data->ID;

							return $media_details;
						}

						return null;
					},
					'mediaItemUrl'        => function () {
						return wp_get_attachment_url( $this->data->ID ) ?: null;
					},
					'mediaType'           => function () {
						return wp_attachment_is_image( $this->data->ID ) ? 'image' : 'file';
					},
					'mimeType'            => function () {
						return ! empty( $this->data->post_mime_type ) ? $this->data->post_mime_type : null;
					},
					'sourceUrl'           => function () {
						return $this->get_source_url_by_size( 'full' );
					},
					// @todo Remove in 1.30.0.
					'sourceUrlsBySize'    => function () {
						_doing_it_wrong(
							self::class . '->sourceUrlsBySize',
							'Use the `sourceUrlBySize` callable instead. This will be removed in the next major release.',
							'1.29.1'
						);

						/**
						 * This returns an empty array on the VIP Go platform.
						 */
						$sizes = get_intermediate_image_sizes(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_intermediate_image_sizes_get_intermediate_image_sizes
						$urls  = [];
						if ( ! empty( $sizes ) && is_array( $sizes ) ) {
							foreach ( $sizes as $size ) {
								$urls[ $size ] = $this->get_source_url_by_size( $size );
							}
						}

						return $urls;
					},
				];

				$this->fields = array_merge( $this->fields, $attachment_fields );
			}

			// Deprecated.
			if ( isset( $this->post_type_object ) && isset( $this->post_type_object->graphql_single_name ) ) {
				$type_id                  = $this->post_type_object->graphql_single_name . 'Id';
				$this->fields[ $type_id ] = function () {
					return absint( $this->data->ID );
				};
			}
		}
	}

	/**
	 * Gets the source URL for an image attachment by size.
	 *
	 * This method caches the source URL for a given size to prevent multiple calls to `wp_get_attachment_image_src`.
	 *
	 * @param ?string $size The size of the image to get the source URL for. `full` by default.
	 */
	public function get_source_url_by_size( ?string $size = 'full' ): ?string {
		// If size is not set, default to 'full'.
		if ( ! $size ) {
			$size = 'full';
		}

		// Resolve the source URL for the size if it hasn't been resolved yet.
		if ( ! array_key_exists( $size, $this->source_urls_by_size ) ) {
			$src = wp_get_attachment_image_src( $this->data->ID, $size );

			$this->source_urls_by_size[ $size ] = ! empty( $src ) ? $src[0] : null;
		}

		return $this->source_urls_by_size[ $size ];
	}
}
