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 / Model / Post.php

Post.php in WPGraphQL trunk, at src/Model/Post.php

930 lines 30.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Model - PostObject
4 *
5 * @package WPGraphQL\Model
6 */
7
8 namespace WPGraphQL\Model;
9
10 use GraphQLRelay\Relay;
11 use WPGraphQL\Utils\Utils;
12 use WP_Post;
13
14 /**
15 * Class Post - Models data for the Post object type
16 *
17 * @property ?int $authorDatabaseId
18 * @property ?string $authorId
19 * @property int $commentCount
20 * @property ?string $commentStatus
21 * @property ?string $contentRaw
22 * @property ?string $contentRendered
23 * @property ?int $databaseId
24 * @property ?string $date
25 * @property ?string $dateGmt
26 * @property ?int $editLastId
27 * @property string[]|null $editLock
28 * @property ?string $enclosure
29 * @property ?string $excerptRaw
30 * @property ?string $excerptRendered
31 * @property ?int $featuredImageDatabaseId
32 * @property ?string $featuredImageId
33 * @property ?string $guid
34 * @property bool $hasPassword
35 * @property ?string $id
36 * @property bool $isFrontPage
37 * @property bool $isPostsPage
38 * @property bool $isPreview
39 * @property bool $isPrivacyPage
40 * @property bool $isRevision
41 * @property bool $isSticky
42 * @property ?string $link
43 * @property ?int $menuOrder
44 * @property ?string $modified
45 * @property ?string $modifiedGmt
46 * @property ?string $pageTemplate
47 * @property ?int $parentDatabaseId
48 * @property ?string $parentId
49 * @property ?string $password
50 * @property ?string $pinged
51 * @property ?string $pingStatus
52 * @property ?string $post_type
53 * @property int $previewRevisionDatabaseId
54 * @property ?string $slug
55 * @property ?string $status
56 * @property array{
57 * __typename: string,
58 * templateName: string
59 * } $template
60 * @property ?string $titleRaw
61 * @property ?string $titleRendered
62 * @property ?string $toPing
63 * @property ?string $uri
64 *
65 * Attachment specific fields:
66 * @property string|null $altText
67 * @property string|null $captionRaw
68 * @property string|null $captionRendered
69 * @property string|null $descriptionRaw
70 * @property string|null $descriptionRendered
71 * @property array<string,mixed>|null $mediaDetails
72 * @property string|null $mediaItemUrl
73 * @property string|null $mediaType
74 * @property string|null $mimeType
75 * @property string|null $sourceUrl
76 *
77 * Aliases:
78 * @property ?int $ID
79 * @property ?int $post_author
80 * @property ?string $post_status
81 *
82 * @extends \WPGraphQL\Model\Model<\WP_Post>
83 */
84 class Post extends Model {
85 /**
86 * Store the global post to reset during model tear down
87 *
88 * @var \WP_Post
89 */
90 protected $global_post;
91
92 /**
93 * Stores the incoming post type object for the post being modeled
94 *
95 * @var \WP_Post_Type|null $post_type_object
96 */
97 protected $post_type_object;
98
99 /**
100 * Store the instance of the WP_Query
101 *
102 * @var \WP_Query
103 */
104 protected $wp_query;
105
106 /**
107 * Stores the resolved image `sourceUrl`s keyed by size.
108 *
109 * This is used to prevent multiple calls to `wp_get_attachment_image_src`.
110 *
111 * If no source URL is found for a size, the value will be `null`.
112 *
113 * @var array<string,?string>
114 */
115 protected $source_urls_by_size = [];
116
117 /**
118 * Post constructor.
119 *
120 * @param \WP_Post $post The incoming WP_Post object that needs modeling.
121 *
122 * @return void
123 */
124 public function __construct( WP_Post $post ) {
125
126 /**
127 * Set the data as the Post object
128 */
129 $this->data = $post;
130 $this->post_type_object = get_post_type_object( $post->post_type );
131
132 /**
133 * If the post type is 'revision', we need to get the post_type_object
134 * of the parent post type to determine capabilities from
135 */
136 if ( 'revision' === $post->post_type && ! empty( $post->post_parent ) ) {
137 $parent = get_post( absint( $post->post_parent ) );
138 if ( ! empty( $parent ) ) {
139 $this->post_type_object = get_post_type_object( $parent->post_type );
140 }
141 }
142
143 /**
144 * Mimic core functionality for templates, as seen here:
145 * https://github.com/WordPress/WordPress/blob/6fd8080e7ee7599b36d4528f72a8ced612130b8c/wp-includes/template-loader.php#L56
146 */
147 if ( 'attachment' === $this->data->post_type ) {
148 remove_filter( 'the_content', 'prepend_attachment' );
149 }
150
151 $allowed_restricted_fields = [
152 'databaseId',
153 'enqueuedScriptsQueue',
154 'enqueuedStylesheetsQueue',
155 'hasPassword',
156 'id',
157 'isFrontPage',
158 'isPostsPage',
159 'isPrivacyPage',
160 'isRestricted',
161 'link',
162 'post_status',
163 'post_type',
164 'slug',
165 'status',
166 'titleRendered',
167 'uri',
168 ];
169
170 if ( isset( $this->post_type_object->graphql_single_name ) ) {
171 $allowed_restricted_fields[] = $this->post_type_object->graphql_single_name . 'Id';
172 }
173
174 $restricted_cap = $this->get_restricted_cap();
175
176 parent::__construct( $restricted_cap, $allowed_restricted_fields, (int) $post->post_author );
177 }
178
179 /**
180 * {@inheritDoc}
181 */
182 public function setup() {
183 global $wp_query, $post;
184
185 /**
186 * Store the global post before overriding
187 */
188 $this->global_post = $post;
189
190 /**
191 * Set the resolving post to the global $post. That way any filters that
192 * might be applied when resolving fields can rely on global post and
193 * post data being set up.
194 */
195 if ( $this->data instanceof WP_Post ) {
196 $id = $this->data->ID;
197 $post_type = $this->data->post_type;
198 $post_name = $this->data->post_name;
199 $data = $this->data;
200
201 if ( 'revision' === $this->data->post_type ) {
202 $id = $this->data->post_parent;
203 $parent = get_post( $this->data->post_parent );
204 if ( empty( $parent ) ) {
205 $this->fields = [];
206 return;
207 }
208 $post_type = $parent->post_type;
209 $post_name = $parent->post_name;
210 $data = $parent;
211 }
212
213 /**
214 * Clear out existing postdata
215 */
216 $wp_query->reset_postdata();
217
218 /**
219 * Parse the query to tell WordPress how to
220 * setup global state
221 */
222 if ( 'post' === $post_type ) {
223 $wp_query->parse_query(
224 [
225 'page' => '',
226 'p' => $id,
227 ]
228 );
229 } elseif ( 'page' === $post_type ) {
230 $wp_query->parse_query(
231 [
232 'page' => '',
233 'pagename' => $post_name,
234 ]
235 );
236 } elseif ( 'attachment' === $post_type ) {
237 $wp_query->parse_query(
238 [
239 'attachment' => $post_name,
240 ]
241 );
242 } else {
243 $wp_query->parse_query(
244 [
245 $post_type => $post_name,
246 'post_type' => $post_type,
247 'name' => $post_name,
248 ]
249 );
250 }
251
252 $wp_query->setup_postdata( $data );
253 $GLOBALS['post'] = $data; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
254 $wp_query->queried_object = get_post( $this->data->ID );
255 $wp_query->queried_object_id = $this->data->ID;
256 }
257 }
258
259 /**
260 * Retrieve the cap to check if the data should be restricted for the post
261 *
262 * @return string
263 */
264 protected function get_restricted_cap() {
265 if ( ! empty( $this->data->post_password ) ) {
266 return isset( $this->post_type_object->cap->edit_others_posts ) ? $this->post_type_object->cap->edit_others_posts : 'edit_others_posts';
267 }
268
269 switch ( $this->data->post_status ) {
270 case 'trash':
271 $cap = isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts';
272 break;
273 case 'draft':
274 case 'future':
275 case 'pending':
276 $cap = isset( $this->post_type_object->cap->edit_others_posts ) ? $this->post_type_object->cap->edit_others_posts : 'edit_others_posts';
277 break;
278 default:
279 $cap = '';
280 break;
281 }
282
283 return $cap;
284 }
285
286 /**
287 * {@inheritDoc}
288 */
289 public function is_private() {
290
291 /**
292 * If the post is of post_type "revision", we need to access the parent of the Post
293 * so that we can check access rights of the parent post. Revision access is inherit
294 * to the Parent it is a revision of.
295 */
296 if ( 'revision' === $this->data->post_type ) {
297
298 // Get the post
299 $parent_post = get_post( $this->data->post_parent );
300
301 // If the parent post doesn't exist, the revision should be considered private
302 if ( ! $parent_post instanceof WP_Post ) {
303 return true;
304 }
305
306 // Determine if the revision is private using capabilities relative to the parent
307 return $this->is_post_private( $parent_post );
308 }
309
310 /**
311 * Media Items (attachments) with "inherit" status should inherit their privacy
312 * from their parent post. If the parent is draft/pending/private, the attachment
313 * should also be considered private.
314 *
315 * Attachments without a parent or with a published parent are public. Once uploaded
316 * to the media library they are exposed with a public URL on the site.
317 *
318 * The WP REST API sets media items to private if they don't have a `post_parent` set, but
319 * this has broken production apps, because media items can be uploaded directly to the
320 * media library and published as a featured image, published inline within content, or
321 * within a Gutenberg block, etc, but then a consumer tries to ask for data of a published
322 * image and REST returns nothing because the media item is treated as private.
323 *
324 * For attachments with "inherit" status, we check the parent's privacy status.
325 * For other attachments, they are treated as public.
326 *
327 * To override this behavior and make all media items public (regardless of parent status),
328 * use the `graphql_pre_model_data_is_private` filter:
329 *
330 * @example
331 * ```php
332 * add_filter( 'graphql_pre_model_data_is_private', function( $is_private, $model_name, $data ) {
333 * // Make all media items public
334 * if ( 'PostObject' === $model_name && isset( $data->post_type ) && 'attachment' === $data->post_type ) {
335 * return false; // false = not private (public)
336 * }
337 * return $is_private; // Return null to use default logic for other types
338 * }, 10, 3 );
339 * ```
340 */
341 if ( 'attachment' === $this->data->post_type ) {
342 // If the attachment has "inherit" status and a parent post, check the parent's privacy
343 // This ensures attachments inherit visibility from their parent (e.g., draft post = private attachment)
344 if ( 'inherit' === $this->data->post_status && ! empty( $this->data->post_parent ) ) {
345 $parent_post = get_post( (int) $this->data->post_parent );
346
347 // If parent doesn't exist (e.g., was deleted), treat as public
348 // This aligns with the documented reasoning: attachments without parents are public
349 // because they may be used in published content, featured images, etc.
350 if ( ! $parent_post instanceof WP_Post ) {
351 return false;
352 }
353
354 // Check if the parent post would be private
355 // Create a temporary Post model to check the parent's privacy
356 // This properly initializes the parent's post type object and checks its privacy status
357 $parent_model = new self( $parent_post );
358 return $parent_model->is_private();
359 }
360
361 // Attachments without inherit status or without a parent are public
362 // This preserves the documented behavior: media items uploaded directly to the library
363 // or used in published content should be publicly accessible
364 return false;
365 }
366
367 /**
368 * Published content is public, not private
369 */
370 if ( 'publish' === $this->data->post_status && $this->post_type_object && ( true === $this->post_type_object->public || true === $this->post_type_object->publicly_queryable ) ) {
371 return false;
372 }
373
374 return $this->is_post_private( $this->data );
375 }
376
377 /**
378 * Method for determining if the data should be considered private or not
379 *
380 * @param \WP_Post $post_object The object of the post we need to verify permissions for
381 *
382 * @return bool
383 */
384 protected function is_post_private( $post_object = null ) {
385 $post_type_object = $this->post_type_object;
386
387 if ( ! $post_type_object ) {
388 return true;
389 }
390
391 if ( ! $post_object ) {
392 $post_object = $this->data;
393 }
394
395 $status = $this->data->post_status;
396 $status_object = get_post_status_object( $status );
397
398 /**
399 * A status whose `public` flag is true is visible to everyone, the same way it is on the
400 * WordPress front-end and the REST API (e.g. `publish` and custom statuses registered with
401 * `'public' => true`). Revisions are excluded here, their access is determined relative to
402 * the parent post below.
403 */
404 if (
405 'revision' !== $this->data->post_type &&
406 $status_object instanceof \stdClass &&
407 true === $status_object->public &&
408 ( true === $post_type_object->public || true === $post_type_object->publicly_queryable )
409 ) {
410 return false;
411 }
412
413 /**
414 * Otherwise the post is private unless the user has the capability required for its status:
415 * `read_private_posts` for the `private` status, and `edit_posts` for every other non-public
416 * status (draft, pending, future, trash, and custom non-public statuses). This lets users who
417 * can read private posts see them even without edit capabilities.
418 */
419 $required_cap = 'private' === $status
420 ? ( isset( $post_type_object->cap->read_private_posts ) ? $post_type_object->cap->read_private_posts : 'read_private_posts' )
421 : ( isset( $post_type_object->cap->edit_posts ) ? $post_type_object->cap->edit_posts : 'edit_posts' );
422
423 if ( ! current_user_can( $required_cap ) ) {
424 return true;
425 }
426
427 /**
428 * If the owner of the content is the current user
429 */
430 if ( ( true === $this->owner_matches_current_user() ) && 'revision' !== $post_object->post_type ) {
431 return false;
432 }
433
434 /**
435 * If the post_type isn't (not registered) or is not allowed in WPGraphQL,
436 * mark the post as private
437 */
438
439 if ( empty( $post_type_object->name ) || ! in_array( $post_type_object->name, \WPGraphQL::get_allowed_post_types(), true ) ) {
440 return true;
441 }
442
443 if ( 'revision' === $this->data->post_type || 'auto-draft' === $this->data->post_status ) {
444 $parent = get_post( (int) $this->data->post_parent );
445
446 if ( empty( $parent ) ) {
447 return true;
448 }
449
450 $parent_post_type_obj = $post_type_object;
451
452 if ( 'private' === $parent->post_status ) {
453 $cap = isset( $parent_post_type_obj->cap->read_private_posts ) ? $parent_post_type_obj->cap->read_private_posts : 'read_private_posts';
454 } else {
455 $cap = isset( $parent_post_type_obj->cap->edit_post ) ? $parent_post_type_obj->cap->edit_post : 'edit_post';
456 }
457
458 if ( ! current_user_can( $cap, $parent->ID ) ) {
459 return true;
460 }
461 }
462
463 return false;
464 }
465
466 /**
467 * {@inheritDoc}
468 */
469 protected function init() {
470 if ( empty( $this->fields ) ) {
471 $this->fields = [
472 'authorDatabaseId' => function () {
473 if ( true === $this->isPreview ) {
474 $parent_post = get_post( $this->data->post_parent );
475
476 return ! empty( $parent_post->post_author ) ? (int) $parent_post->post_author : null;
477 }
478
479 return ! empty( $this->data->post_author ) ? (int) $this->data->post_author : null;
480 },
481 'authorId' => function () {
482 return ! empty( $this->authorDatabaseId ) ? Relay::toGlobalId( 'user', (string) $this->authorDatabaseId ) : null;
483 },
484 'commentCount' => function () {
485 return ! empty( $this->data->comment_count ) ? absint( $this->data->comment_count ) : 0;
486 },
487 'commentStatus' => function () {
488 return ! empty( $this->data->comment_status ) ? $this->data->comment_status : null;
489 },
490 'contentRaw' => [
491 'callback' => function () {
492 return ! empty( $this->data->post_content ) ? $this->data->post_content : null;
493 },
494 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
495 ],
496 'contentRendered' => function () {
497 $content = ! empty( $this->data->post_content ) ? $this->data->post_content : null;
498
499 return ! empty( $content ) ? $this->html_entity_decode( apply_filters( 'the_content', $content ), 'contentRendered', false ) : null;
500 },
501 'databaseId' => function () {
502 return ! empty( $this->data->ID ) ? absint( $this->data->ID ) : null;
503 },
504 'date' => function () {
505 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;
506 },
507 'dateGmt' => function () {
508 return ! empty( $this->data->post_date_gmt ) ? Utils::prepare_date_response( $this->data->post_date_gmt ) : null;
509 },
510 'editLastId' => function () {
511 $edit_last = get_post_meta( $this->data->ID, '_edit_last', true );
512
513 return ! empty( $edit_last ) ? absint( $edit_last ) : null;
514 },
515 'editLock' => function () {
516 if ( ! function_exists( 'wp_check_post_lock' ) ) {
517 require_once ABSPATH . 'wp-admin/includes/post.php';
518 }
519
520 if ( ! wp_check_post_lock( $this->data->ID ) ) {
521 return null;
522 }
523
524 $edit_lock = get_post_meta( $this->data->ID, '_edit_lock', true );
525 $edit_lock_parts = ! empty( $edit_lock ) ? explode( ':', $edit_lock ) : null;
526
527 return ! empty( $edit_lock_parts ) ? $edit_lock_parts : null;
528 },
529 'enclosure' => function () {
530 $enclosure = get_post_meta( $this->data->ID, 'enclosure', true );
531
532 return ! empty( $enclosure ) ? $enclosure : null;
533 },
534 'enqueuedScriptsQueue' => static function () {
535 global $wp_scripts;
536 do_action( 'wp_enqueue_scripts' );
537 $queue = $wp_scripts->queue;
538 $wp_scripts->reset();
539 $wp_scripts->queue = [];
540
541 return $queue;
542 },
543 'enqueuedStylesheetsQueue' => static function () {
544 global $wp_styles;
545 do_action( 'wp_enqueue_scripts' );
546 $queue = $wp_styles->queue;
547 $wp_styles->reset();
548 $wp_styles->queue = [];
549
550 return $queue;
551 },
552 'excerptRaw' => [
553 'callback' => function () {
554 return ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : null;
555 },
556 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
557 ],
558 'excerptRendered' => function () {
559 $excerpt = ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : '';
560 $excerpt = apply_filters( 'get_the_excerpt', $excerpt, $this->data );
561
562 return $this->html_entity_decode( apply_filters( 'the_excerpt', $excerpt ), 'excerptRendered' );
563 },
564 'featuredImageDatabaseId' => function () {
565 if ( $this->isRevision ) {
566 $id = $this->parentDatabaseId;
567 } else {
568 $id = $this->data->ID;
569 }
570
571 $thumbnail_id = get_post_thumbnail_id( $id );
572
573 return ! empty( $thumbnail_id ) ? absint( $thumbnail_id ) : null;
574 },
575 'featuredImageId' => function () {
576 return ! empty( $this->featuredImageDatabaseId ) ? Relay::toGlobalId( 'post', (string) $this->featuredImageDatabaseId ) : null;
577 },
578 'guid' => function () {
579 return ! empty( $this->data->guid ) ? $this->data->guid : null;
580 },
581 'hasPassword' => function () {
582 return ! empty( $this->data->post_password );
583 },
584 'id' => function () {
585 return ! empty( $this->data->post_type && ! empty( $this->databaseId ) ) ? Relay::toGlobalId( 'post', (string) $this->databaseId ) : null;
586 },
587 'isFrontPage' => function () {
588 if ( 'page' !== $this->data->post_type || 'page' !== get_option( 'show_on_front' ) ) {
589 return false;
590 }
591 if ( absint( get_option( 'page_on_front', 0 ) ) === $this->data->ID ) {
592 return true;
593 }
594
595 return false;
596 },
597 'isPostsPage' => function () {
598 if ( 'page' !== $this->data->post_type ) {
599 return false;
600 }
601 if ( 'posts' !== get_option( 'show_on_front', 'posts' ) && absint( get_option( 'page_for_posts', 0 ) ) === $this->data->ID ) {
602 return true;
603 }
604
605 return false;
606 },
607 'isPreview' => function () {
608 if ( $this->isRevision ) {
609 $revisions = wp_get_post_revisions(
610 $this->data->post_parent,
611 [
612 'posts_per_page' => 1,
613 'fields' => 'ids',
614 'check_enabled' => false,
615 ]
616 );
617
618 if ( in_array( $this->data->ID, array_values( $revisions ), true ) ) {
619 return true;
620 }
621 }
622
623 if ( ! post_type_supports( $this->data->post_type, 'revisions' ) && 'draft' === $this->data->post_status ) {
624 return true;
625 }
626
627 return false;
628 },
629 'isPrivacyPage' => function () {
630 if ( 'page' !== $this->data->post_type ) {
631 return false;
632 }
633 if ( absint( get_option( 'wp_page_for_privacy_policy', 0 ) ) === $this->data->ID ) {
634 return true;
635 }
636
637 return false;
638 },
639 'isRevision' => function () {
640 return 'revision' === $this->data->post_type;
641 },
642 'isSticky' => function () {
643 return is_sticky( $this->data->ID );
644 },
645 'link' => function () {
646 $link = get_permalink( $this->data->ID );
647
648 if ( $this->isPreview ) {
649 $link = get_preview_post_link( $this->parentDatabaseId );
650 } elseif ( $this->isRevision ) {
651 $link = get_permalink( $this->data->ID );
652 }
653
654 return ! empty( $link ) ? urldecode( $link ) : null;
655 },
656 'menuOrder' => function () {
657 return ! empty( $this->data->menu_order ) ? absint( $this->data->menu_order ) : null;
658 },
659 'modified' => function () {
660 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;
661 },
662 'modifiedGmt' => function () {
663 return ! empty( $this->data->post_modified_gmt ) ? Utils::prepare_date_response( $this->data->post_modified_gmt ) : null;
664 },
665 'pageTemplate' => function () {
666 $slug = get_page_template_slug( $this->data->ID );
667
668 return ! empty( $slug ) ? $slug : null;
669 },
670 'parentDatabaseId' => function () {
671 return ! empty( $this->data->post_parent ) ? absint( $this->data->post_parent ) : null;
672 },
673 'parentId' => function () {
674 return ( ! empty( $this->data->post_type ) && ! empty( $this->parentDatabaseId ) ) ? Relay::toGlobalId( 'post', (string) $this->parentDatabaseId ) : null;
675 },
676 'password' => function () {
677 return ! empty( $this->data->post_password ) ? $this->data->post_password : null;
678 },
679 'pinged' => function () {
680 $punged = get_pung( $this->data->ID );
681
682 return empty( $punged ) ? null : implode( ',', (array) $punged );
683 },
684 'pingStatus' => function () {
685 return ! empty( $this->data->ping_status ) ? $this->data->ping_status : null;
686 },
687 'post_type' => function () {
688 return ! empty( $this->data->post_type ) ? $this->data->post_type : null;
689 },
690 'previewRevisionDatabaseId' => [
691 'callback' => function () {
692 $revisions = wp_get_post_revisions(
693 $this->data->ID,
694 [
695 'posts_per_page' => 1,
696 'fields' => 'ids',
697 'check_enabled' => false,
698 ]
699 );
700
701 return is_array( $revisions ) && ! empty( $revisions ) ? array_values( $revisions )[0] : null;
702 },
703 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
704 ],
705 'previewRevisionId' => function () {
706 return ! empty( $this->previewRevisionDatabaseId ) ? Relay::toGlobalId( 'post', (string) $this->previewRevisionDatabaseId ) : null;
707 },
708 'slug' => function () {
709 return ! empty( $this->data->post_name ) ? urldecode( $this->data->post_name ) : null;
710 },
711 'status' => function () {
712 return ! empty( $this->data->post_status ) ? $this->data->post_status : null;
713 },
714 'template' => function () {
715 $registered_templates = wp_get_theme()->get_page_templates( null, $this->data->post_type );
716
717 $template = [
718 '__typename' => 'DefaultTemplate',
719 'templateName' => 'Default',
720 ];
721
722 if ( true === $this->isPreview ) {
723 $parent_post = get_post( $this->parentDatabaseId );
724
725 if ( empty( $parent_post ) ) {
726 return $template;
727 }
728
729 /** @var \WP_Post $parent_post */
730 $registered_templates = wp_get_theme()->get_page_templates( $parent_post );
731
732 if ( empty( $registered_templates ) ) {
733 return $template;
734 }
735 $set_template = get_post_meta( $parent_post->ID, '_wp_page_template', true );
736 $template_name = get_page_template_slug( $parent_post->ID );
737
738 if ( empty( $set_template ) ) {
739 $set_template = get_post_meta( $this->data->ID, '_wp_page_template', true );
740 }
741
742 if ( empty( $template_name ) ) {
743 $template_name = get_page_template_slug( $this->data->ID );
744 }
745
746 $template_name = ! empty( $template_name ) ? $template_name : 'Default';
747 } else {
748 if ( empty( $registered_templates ) ) {
749 return $template;
750 }
751
752 $set_template = get_post_meta( $this->data->ID, '_wp_page_template', true );
753 $template_name = get_page_template_slug( $this->data->ID );
754
755 $template_name = ! empty( $template_name ) ? $template_name : 'Default';
756 }
757
758 if ( ! empty( $registered_templates[ $set_template ] ) ) {
759 $name = Utils::format_type_name_for_wp_template( $registered_templates[ $set_template ], $set_template );
760
761 // If the name is empty, fallback to DefaultTemplate
762 if ( empty( $name ) ) {
763 $name = 'DefaultTemplate';
764 }
765
766 $template = [
767 '__typename' => $name,
768 'templateName' => ucwords( $registered_templates[ $set_template ] ),
769 ];
770 }
771
772 return $template;
773 },
774 'titleRaw' => [
775 'callback' => function () {
776 return ! empty( $this->data->post_title ) ? $this->data->post_title : null;
777 },
778 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
779 ],
780 'titleRendered' => function () {
781 $id = ! empty( $this->data->ID ) ? $this->data->ID : null;
782 $title = ! empty( $this->data->post_title ) ? $this->data->post_title : '';
783
784 $processedTitle = ! empty( $title ) ? $this->html_entity_decode( apply_filters( 'the_title', $title, $id ), 'titleRendered', true ) : '';
785
786 return empty( $processedTitle ) ? null : $processedTitle;
787 },
788 'toPing' => function () {
789 $to_ping = get_to_ping( $this->data->ID );
790
791 return ! empty( $to_ping ) ? implode( ',', (array) $to_ping ) : null;
792 },
793 'uri' => function () {
794 $uri = $this->link;
795
796 if ( true === $this->isFrontPage ) {
797 return '/';
798 }
799
800 // if the page is set as the posts page
801 // the page node itself is not identifiable
802 // by URI. Instead, the uri would return the
803 // Post content type as that uri
804 // represents the blog archive instead of a page
805 if ( true === $this->isPostsPage ) {
806 return null;
807 }
808
809 return ! empty( $uri ) ? str_ireplace( home_url(), '', $uri ) : null;
810 },
811
812 // Aliases.
813 'ID' => function () {
814 return $this->databaseId;
815 },
816 'post_author' => function () {
817 return $this->authorDatabaseId;
818 },
819 'post_status' => function () {
820 return $this->status;
821 },
822 ];
823
824 if ( 'attachment' === $this->data->post_type ) {
825 $attachment_fields = [
826 'altText' => function () {
827 return get_post_meta( $this->data->ID, '_wp_attachment_image_alt', true );
828 },
829 'captionRaw' => [
830 'callback' => function () {
831 return ! empty( $this->data->post_excerpt ) ? $this->data->post_excerpt : null;
832 },
833 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
834 ],
835 'captionRendered' => function () {
836 $caption = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $this->data->post_excerpt, $this->data ) );
837
838 return ! empty( $caption ) ? $caption : null;
839 },
840 'descriptionRaw' => [
841 'callback' => function () {
842 return ! empty( $this->data->post_content ) ? $this->data->post_content : null;
843 },
844 'capability' => isset( $this->post_type_object->cap->edit_posts ) ? $this->post_type_object->cap->edit_posts : 'edit_posts',
845 ],
846 'descriptionRendered' => function () {
847 return ! empty( $this->data->post_content ) ? apply_filters( 'the_content', $this->data->post_content ) : null;
848 },
849 'mediaDetails' => function () {
850 $media_details = wp_get_attachment_metadata( $this->data->ID );
851 if ( ! empty( $media_details ) ) {
852 $media_details['ID'] = $this->data->ID;
853
854 return $media_details;
855 }
856
857 return null;
858 },
859 'mediaItemUrl' => function () {
860 return wp_get_attachment_url( $this->data->ID ) ?: null;
861 },
862 'mediaType' => function () {
863 return wp_attachment_is_image( $this->data->ID ) ? 'image' : 'file';
864 },
865 'mimeType' => function () {
866 return ! empty( $this->data->post_mime_type ) ? $this->data->post_mime_type : null;
867 },
868 'sourceUrl' => function () {
869 return $this->get_source_url_by_size( 'full' );
870 },
871 // @todo Remove in 1.30.0.
872 'sourceUrlsBySize' => function () {
873 _doing_it_wrong(
874 self::class . '->sourceUrlsBySize',
875 'Use the `sourceUrlBySize` callable instead. This will be removed in the next major release.',
876 '1.29.1'
877 );
878
879 /**
880 * This returns an empty array on the VIP Go platform.
881 */
882 $sizes = get_intermediate_image_sizes(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_intermediate_image_sizes_get_intermediate_image_sizes
883 $urls = [];
884 if ( ! empty( $sizes ) && is_array( $sizes ) ) {
885 foreach ( $sizes as $size ) {
886 $urls[ $size ] = $this->get_source_url_by_size( $size );
887 }
888 }
889
890 return $urls;
891 },
892 ];
893
894 $this->fields = array_merge( $this->fields, $attachment_fields );
895 }
896
897 // Deprecated.
898 if ( isset( $this->post_type_object ) && isset( $this->post_type_object->graphql_single_name ) ) {
899 $type_id = $this->post_type_object->graphql_single_name . 'Id';
900 $this->fields[ $type_id ] = function () {
901 return absint( $this->data->ID );
902 };
903 }
904 }
905 }
906
907 /**
908 * Gets the source URL for an image attachment by size.
909 *
910 * This method caches the source URL for a given size to prevent multiple calls to `wp_get_attachment_image_src`.
911 *
912 * @param ?string $size The size of the image to get the source URL for. `full` by default.
913 */
914 public function get_source_url_by_size( ?string $size = 'full' ): ?string {
915 // If size is not set, default to 'full'.
916 if ( ! $size ) {
917 $size = 'full';
918 }
919
920 // Resolve the source URL for the size if it hasn't been resolved yet.
921 if ( ! array_key_exists( $size, $this->source_urls_by_size ) ) {
922 $src = wp_get_attachment_image_src( $this->data->ID, $size );
923
924 $this->source_urls_by_size[ $size ] = ! empty( $src ) ? $src[0] : null;
925 }
926
927 return $this->source_urls_by_size[ $size ];
928 }
929 }
930