1<?php
2/**
3 * Core Post API
4 *
5 * @package WordPress
6 * @subpackage Post
7 */
8
9//
10// Post Type registration.
11//
12
13/**
14 * Creates the initial post types when 'init' action is fired.
15 *
16 * See {@see 'init'}.
17 *
18 * @since 2.9.0
19 */
20function create_initial_post_types() {
21 WP_Post_Type::reset_default_labels();
22
23 register_post_type(
24 'post',
25 array(
26 'labels' => array(
27 'name_admin_bar' => _x( 'Post', 'add new from admin bar' ),
28 ),
29 'public' => true,
30 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
31 '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
32 'capability_type' => 'post',
33 'map_meta_cap' => true,
34 'menu_position' => 5,
35 'menu_icon' => 'dashicons-admin-post',
36 'hierarchical' => false,
37 'rewrite' => false,
38 'query_var' => false,
39 'delete_with_user' => true,
40 'supports' => array(
41 'title',
42 'editor' => array( 'notes' => true ),
43 'author',
44 'thumbnail',
45 'excerpt',
46 'trackbacks',
47 'custom-fields',
48 'comments',
49 'revisions',
50 'post-formats',
51 ),
52 'show_in_rest' => true,
53 'rest_base' => 'posts',
54 'rest_controller_class' => 'WP_REST_Posts_Controller',
55 )
56 );
57
58 register_post_type(
59 'page',
60 array(
61 'labels' => array(
62 'name_admin_bar' => _x( 'Page', 'add new from admin bar' ),
63 ),
64 'public' => true,
65 'publicly_queryable' => false,
66 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
67 '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
68 'capability_type' => 'page',
69 'map_meta_cap' => true,
70 'menu_position' => 20,
71 'menu_icon' => 'dashicons-admin-page',
72 'hierarchical' => true,
73 'rewrite' => false,
74 'query_var' => false,
75 'delete_with_user' => true,
76 'supports' => array(
77 'title',
78 'editor' => array( 'notes' => true ),
79 'author',
80 'thumbnail',
81 'page-attributes',
82 'custom-fields',
83 'comments',
84 'revisions',
85 ),
86 'show_in_rest' => true,
87 'rest_base' => 'pages',
88 'rest_controller_class' => 'WP_REST_Posts_Controller',
89 )
90 );
91
92 register_post_type(
93 'attachment',
94 array(
95 'labels' => array(
96 'name' => _x( 'Media', 'post type general name' ),
97 'name_admin_bar' => _x( 'Media', 'add new from admin bar' ),
98 'add_new' => __( 'Add Media File' ),
99 'add_new_item' => __( 'Add Media File' ),
100 'edit_item' => __( 'Edit Media' ),
101 'view_item' => ( '1' === get_option( 'wp_attachment_pages_enabled' ) ) ? __( 'View Attachment Page' ) : __( 'View Media File' ),
102 'attributes' => __( 'Attachment Attributes' ),
103 ),
104 'public' => true,
105 'show_ui' => true,
106 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
107 '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
108 'capability_type' => 'post',
109 'capabilities' => array(
110 'create_posts' => 'upload_files',
111 ),
112 'map_meta_cap' => true,
113 'menu_icon' => 'dashicons-admin-media',
114 'hierarchical' => false,
115 'rewrite' => false,
116 'query_var' => false,
117 'show_in_nav_menus' => false,
118 'delete_with_user' => true,
119 'supports' => array( 'title', 'author', 'comments' ),
120 'show_in_rest' => true,
121 'rest_base' => 'media',
122 'rest_controller_class' => 'WP_REST_Attachments_Controller',
123 )
124 );
125 add_post_type_support( 'attachment:audio', 'thumbnail' );
126 add_post_type_support( 'attachment:video', 'thumbnail' );
127
128 register_post_type(
129 'revision',
130 array(
131 'labels' => array(
132 'name' => __( 'Revisions' ),
133 'singular_name' => __( 'Revision' ),
134 ),
135 'public' => false,
136 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
137 '_edit_link' => 'revision.php?revision=%d', /* internal use only. don't use this when registering your own post type. */
138 'capability_type' => 'post',
139 'map_meta_cap' => true,
140 'hierarchical' => false,
141 'rewrite' => false,
142 'query_var' => false,
143 'can_export' => false,
144 'delete_with_user' => true,
145 'supports' => array( 'author' ),
146 )
147 );
148
149 register_post_type(
150 'nav_menu_item',
151 array(
152 'labels' => array(
153 'name' => __( 'Navigation Menu Items' ),
154 'singular_name' => __( 'Navigation Menu Item' ),
155 ),
156 'public' => false,
157 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
158 'hierarchical' => false,
159 'rewrite' => false,
160 'delete_with_user' => false,
161 'query_var' => false,
162 'map_meta_cap' => true,
163 'capability_type' => array( 'edit_theme_options', 'edit_theme_options' ),
164 'capabilities' => array(
165 // Meta Capabilities.
166 'edit_post' => 'edit_post',
167 'read_post' => 'read_post',
168 'delete_post' => 'delete_post',
169 // Primitive Capabilities.
170 'edit_posts' => 'edit_theme_options',
171 'edit_others_posts' => 'edit_theme_options',
172 'delete_posts' => 'edit_theme_options',
173 'publish_posts' => 'edit_theme_options',
174 'read_private_posts' => 'edit_theme_options',
175 'read' => 'read',
176 'delete_private_posts' => 'edit_theme_options',
177 'delete_published_posts' => 'edit_theme_options',
178 'delete_others_posts' => 'edit_theme_options',
179 'edit_private_posts' => 'edit_theme_options',
180 'edit_published_posts' => 'edit_theme_options',
181 ),
182 'show_in_rest' => true,
183 'rest_base' => 'menu-items',
184 'rest_controller_class' => 'WP_REST_Menu_Items_Controller',
185 )
186 );
187
188 register_post_type(
189 'custom_css',
190 array(
191 'labels' => array(
192 'name' => __( 'Custom CSS' ),
193 'singular_name' => __( 'Custom CSS' ),
194 ),
195 'public' => false,
196 'hierarchical' => false,
197 'rewrite' => false,
198 'query_var' => false,
199 'delete_with_user' => false,
200 'can_export' => true,
201 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
202 'supports' => array( 'title', 'revisions' ),
203 'capabilities' => array(
204 'delete_posts' => 'edit_theme_options',
205 'delete_post' => 'edit_theme_options',
206 'delete_published_posts' => 'edit_theme_options',
207 'delete_private_posts' => 'edit_theme_options',
208 'delete_others_posts' => 'edit_theme_options',
209 'edit_post' => 'edit_css',
210 'edit_posts' => 'edit_css',
211 'edit_others_posts' => 'edit_css',
212 'edit_published_posts' => 'edit_css',
213 'read_post' => 'read',
214 'read_private_posts' => 'read',
215 'publish_posts' => 'edit_theme_options',
216 ),
217 )
218 );
219
220 register_post_type(
221 'customize_changeset',
222 array(
223 'labels' => array(
224 'name' => _x( 'Changesets', 'post type general name' ),
225 'singular_name' => _x( 'Changeset', 'post type singular name' ),
226 'add_new' => __( 'Add Changeset' ),
227 'add_new_item' => __( 'Add Changeset' ),
228 'new_item' => __( 'New Changeset' ),
229 'edit_item' => __( 'Edit Changeset' ),
230 'view_item' => __( 'View Changeset' ),
231 'all_items' => __( 'All Changesets' ),
232 'search_items' => __( 'Search Changesets' ),
233 'not_found' => __( 'No changesets found.' ),
234 'not_found_in_trash' => __( 'No changesets found in Trash.' ),
235 ),
236 'public' => false,
237 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
238 'map_meta_cap' => true,
239 'hierarchical' => false,
240 'rewrite' => false,
241 'query_var' => false,
242 'can_export' => false,
243 'delete_with_user' => false,
244 'supports' => array( 'title', 'author' ),
245 'capability_type' => 'customize_changeset',
246 'capabilities' => array(
247 'create_posts' => 'customize',
248 'delete_others_posts' => 'customize',
249 'delete_post' => 'customize',
250 'delete_posts' => 'customize',
251 'delete_private_posts' => 'customize',
252 'delete_published_posts' => 'customize',
253 'edit_others_posts' => 'customize',
254 'edit_post' => 'customize',
255 'edit_posts' => 'customize',
256 'edit_private_posts' => 'customize',
257 'edit_published_posts' => 'do_not_allow',
258 'publish_posts' => 'customize',
259 'read' => 'read',
260 'read_post' => 'customize',
261 'read_private_posts' => 'customize',
262 ),
263 )
264 );
265
266 register_post_type(
267 'oembed_cache',
268 array(
269 'labels' => array(
270 'name' => __( 'oEmbed Responses' ),
271 'singular_name' => __( 'oEmbed Response' ),
272 ),
273 'public' => false,
274 'hierarchical' => false,
275 'rewrite' => false,
276 'query_var' => false,
277 'delete_with_user' => false,
278 'can_export' => false,
279 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
280 'supports' => array(),
281 )
282 );
283
284 register_post_type(
285 'user_request',
286 array(
287 'labels' => array(
288 'name' => __( 'User Requests' ),
289 'singular_name' => __( 'User Request' ),
290 ),
291 'public' => false,
292 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
293 'hierarchical' => false,
294 'rewrite' => false,
295 'query_var' => false,
296 'can_export' => false,
297 'delete_with_user' => false,
298 'supports' => array(),
299 )
300 );
301
302 register_post_type(
303 'wp_block',
304 array(
305 'labels' => array(
306 'name' => _x( 'Patterns', 'post type general name' ),
307 'singular_name' => _x( 'Pattern', 'post type singular name' ),
308 'add_new' => __( 'Add Pattern' ),
309 'add_new_item' => __( 'Add Pattern' ),
310 'new_item' => __( 'New Pattern' ),
311 'edit_item' => __( 'Edit Block Pattern' ),
312 'view_item' => __( 'View Pattern' ),
313 'view_items' => __( 'View Patterns' ),
314 'all_items' => __( 'All Patterns' ),
315 'search_items' => __( 'Search Patterns' ),
316 'not_found' => __( 'No patterns found.' ),
317 'not_found_in_trash' => __( 'No patterns found in Trash.' ),
318 'filter_items_list' => __( 'Filter patterns list' ),
319 'items_list_navigation' => __( 'Patterns list navigation' ),
320 'items_list' => __( 'Patterns list' ),
321 'item_published' => __( 'Pattern published.' ),
322 'item_published_privately' => __( 'Pattern published privately.' ),
323 'item_reverted_to_draft' => __( 'Pattern reverted to draft.' ),
324 'item_scheduled' => __( 'Pattern scheduled.' ),
325 'item_updated' => __( 'Pattern updated.' ),
326 ),
327 'public' => false,
328 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
329 'show_ui' => true,
330 'show_in_menu' => false,
331 'rewrite' => false,
332 'show_in_rest' => true,
333 'rest_base' => 'blocks',
334 'rest_controller_class' => 'WP_REST_Blocks_Controller',
335 'capability_type' => 'block',
336 'capabilities' => array(
337 // You need to be able to edit posts, in order to read blocks in their raw form.
338 'read' => 'edit_posts',
339 // You need to be able to publish posts, in order to create blocks.
340 'create_posts' => 'publish_posts',
341 'edit_posts' => 'edit_posts',
342 'edit_published_posts' => 'edit_published_posts',
343 'delete_published_posts' => 'delete_published_posts',
344 // Enables trashing draft posts as well.
345 'delete_posts' => 'delete_posts',
346 'edit_others_posts' => 'edit_others_posts',
347 'delete_others_posts' => 'delete_others_posts',
348 ),
349 'map_meta_cap' => true,
350 'supports' => array(
351 'title',
352 'excerpt',
353 'editor',
354 'revisions',
355 'custom-fields',
356 ),
357 )
358 );
359
360 $template_edit_link = 'site-editor.php?' . build_query(
361 array(
362 'postType' => '%s',
363 'postId' => '%s',
364 'canvas' => 'edit',
365 )
366 );
367
368 register_post_type(
369 'wp_template',
370 array(
371 'labels' => array(
372 'name' => _x( 'Templates', 'post type general name' ),
373 'singular_name' => _x( 'Template', 'post type singular name' ),
374 'add_new' => __( 'Add Template' ),
375 'add_new_item' => __( 'Add Template' ),
376 'new_item' => __( 'New Template' ),
377 'edit_item' => __( 'Edit Template' ),
378 'view_item' => __( 'View Template' ),
379 'all_items' => __( 'Templates' ),
380 'search_items' => __( 'Search Templates' ),
381 'parent_item_colon' => __( 'Parent Template:' ),
382 'not_found' => __( 'No templates found.' ),
383 'not_found_in_trash' => __( 'No templates found in Trash.' ),
384 'archives' => __( 'Template archives' ),
385 'insert_into_item' => __( 'Insert into template' ),
386 'uploaded_to_this_item' => __( 'Uploaded to this template' ),
387 'filter_items_list' => __( 'Filter templates list' ),
388 'items_list_navigation' => __( 'Templates list navigation' ),
389 'items_list' => __( 'Templates list' ),
390 'item_updated' => __( 'Template updated.' ),
391 ),
392 'description' => __( 'Templates to include in your theme.' ),
393 'public' => false,
394 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
395 '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */
396 'has_archive' => false,
397 'show_ui' => false,
398 'show_in_menu' => false,
399 'show_in_rest' => true,
400 'rewrite' => false,
401 'rest_base' => 'templates',
402 'rest_controller_class' => 'WP_REST_Templates_Controller',
403 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller',
404 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller',
405 'late_route_registration' => true,
406 'capability_type' => array( 'template', 'templates' ),
407 'capabilities' => array(
408 'create_posts' => 'edit_theme_options',
409 'delete_posts' => 'edit_theme_options',
410 'delete_others_posts' => 'edit_theme_options',
411 'delete_private_posts' => 'edit_theme_options',
412 'delete_published_posts' => 'edit_theme_options',
413 'edit_posts' => 'edit_theme_options',
414 'edit_others_posts' => 'edit_theme_options',
415 'edit_private_posts' => 'edit_theme_options',
416 'edit_published_posts' => 'edit_theme_options',
417 'publish_posts' => 'edit_theme_options',
418 'read' => 'edit_theme_options',
419 'read_private_posts' => 'edit_theme_options',
420 ),
421 'map_meta_cap' => true,
422 'supports' => array(
423 'title',
424 'slug',
425 'excerpt',
426 'editor',
427 'revisions',
428 'author',
429 ),
430 )
431 );
432
433 register_post_type(
434 'wp_template_part',
435 array(
436 'labels' => array(
437 'name' => _x( 'Template Parts', 'post type general name' ),
438 'singular_name' => _x( 'Template Part', 'post type singular name' ),
439 'add_new' => __( 'Add Template Part' ),
440 'add_new_item' => __( 'Add Template Part' ),
441 'new_item' => __( 'New Template Part' ),
442 'edit_item' => __( 'Edit Template Part' ),
443 'view_item' => __( 'View Template Part' ),
444 'all_items' => __( 'Template Parts' ),
445 'search_items' => __( 'Search Template Parts' ),
446 'parent_item_colon' => __( 'Parent Template Part:' ),
447 'not_found' => __( 'No template parts found.' ),
448 'not_found_in_trash' => __( 'No template parts found in Trash.' ),
449 'archives' => __( 'Template part archives' ),
450 'insert_into_item' => __( 'Insert into template part' ),
451 'uploaded_to_this_item' => __( 'Uploaded to this template part' ),
452 'filter_items_list' => __( 'Filter template parts list' ),
453 'items_list_navigation' => __( 'Template parts list navigation' ),
454 'items_list' => __( 'Template parts list' ),
455 'item_updated' => __( 'Template part updated.' ),
456 ),
457 'description' => __( 'Template parts to include in your templates.' ),
458 'public' => false,
459 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
460 '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */
461 'has_archive' => false,
462 'show_ui' => false,
463 'show_in_menu' => false,
464 'show_in_rest' => true,
465 'rewrite' => false,
466 'rest_base' => 'template-parts',
467 'rest_controller_class' => 'WP_REST_Templates_Controller',
468 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller',
469 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller',
470 'late_route_registration' => true,
471 'map_meta_cap' => true,
472 'capabilities' => array(
473 'create_posts' => 'edit_theme_options',
474 'delete_posts' => 'edit_theme_options',
475 'delete_others_posts' => 'edit_theme_options',
476 'delete_private_posts' => 'edit_theme_options',
477 'delete_published_posts' => 'edit_theme_options',
478 'edit_posts' => 'edit_theme_options',
479 'edit_others_posts' => 'edit_theme_options',
480 'edit_private_posts' => 'edit_theme_options',
481 'edit_published_posts' => 'edit_theme_options',
482 'publish_posts' => 'edit_theme_options',
483 'read' => 'edit_theme_options',
484 'read_private_posts' => 'edit_theme_options',
485 ),
486 'supports' => array(
487 'title',
488 'slug',
489 'excerpt',
490 'editor',
491 'revisions',
492 'author',
493 ),
494 )
495 );
496
497 register_post_type(
498 'wp_global_styles',
499 array(
500 'label' => _x( 'Global Styles', 'post type general name' ),
501 'description' => __( 'Global styles to include in themes.' ),
502 'public' => false,
503 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
504 '_edit_link' => '/site-editor.php?canvas=edit', /* internal use only. don't use this when registering your own post type. */
505 'show_ui' => false,
506 'show_in_rest' => true,
507 'rewrite' => false,
508 'rest_base' => 'global-styles',
509 'rest_controller_class' => 'WP_REST_Global_Styles_Controller',
510 'revisions_rest_controller_class' => 'WP_REST_Global_Styles_Revisions_Controller',
511 'late_route_registration' => true,
512 'capabilities' => array(
513 'read' => 'edit_posts',
514 'create_posts' => 'edit_theme_options',
515 'edit_posts' => 'edit_theme_options',
516 'edit_published_posts' => 'edit_theme_options',
517 'delete_published_posts' => 'edit_theme_options',
518 'edit_others_posts' => 'edit_theme_options',
519 'delete_others_posts' => 'edit_theme_options',
520 ),
521 'map_meta_cap' => true,
522 'supports' => array(
523 'title',
524 'editor',
525 'revisions',
526 ),
527 )
528 );
529 // Disable autosave endpoints for global styles.
530 remove_post_type_support( 'wp_global_styles', 'autosave' );
531
532 $navigation_post_edit_link = 'site-editor.php?' . build_query(
533 array(
534 'postId' => '%s',
535 'postType' => 'wp_navigation',
536 'canvas' => 'edit',
537 )
538 );
539
540 register_post_type(
541 'wp_navigation',
542 array(
543 'labels' => array(
544 'name' => _x( 'Navigation Menus', 'post type general name' ),
545 'singular_name' => _x( 'Navigation Menu', 'post type singular name' ),
546 'add_new' => __( 'Add Navigation Menu' ),
547 'add_new_item' => __( 'Add Navigation Menu' ),
548 'new_item' => __( 'New Navigation Menu' ),
549 'edit_item' => __( 'Edit Navigation Menu' ),
550 'view_item' => __( 'View Navigation Menu' ),
551 'all_items' => __( 'Navigation Menus' ),
552 'search_items' => __( 'Search Navigation Menus' ),
553 'parent_item_colon' => __( 'Parent Navigation Menu:' ),
554 'not_found' => __( 'No Navigation Menu found.' ),
555 'not_found_in_trash' => __( 'No Navigation Menu found in Trash.' ),
556 'archives' => __( 'Navigation Menu archives' ),
557 'insert_into_item' => __( 'Insert into Navigation Menu' ),
558 'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu' ),
559 'filter_items_list' => __( 'Filter Navigation Menu list' ),
560 'items_list_navigation' => __( 'Navigation Menus list navigation' ),
561 'items_list' => __( 'Navigation Menus list' ),
562 ),
563 'description' => __( 'Navigation menus that can be inserted into your site.' ),
564 'public' => false,
565 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
566 '_edit_link' => $navigation_post_edit_link, /* internal use only. don't use this when registering your own post type. */
567 'has_archive' => false,
568 'show_ui' => true,
569 'show_in_menu' => false,
570 'show_in_admin_bar' => false,
571 'show_in_rest' => true,
572 'rewrite' => false,
573 'map_meta_cap' => true,
574 'capabilities' => array(
575 'edit_others_posts' => 'edit_theme_options',
576 'delete_posts' => 'edit_theme_options',
577 'publish_posts' => 'edit_theme_options',
578 'create_posts' => 'edit_theme_options',
579 'read_private_posts' => 'edit_theme_options',
580 'delete_private_posts' => 'edit_theme_options',
581 'delete_published_posts' => 'edit_theme_options',
582 'delete_others_posts' => 'edit_theme_options',
583 'edit_private_posts' => 'edit_theme_options',
584 'edit_published_posts' => 'edit_theme_options',
585 'edit_posts' => 'edit_theme_options',
586 ),
587 'rest_base' => 'navigation',
588 'rest_controller_class' => 'WP_REST_Posts_Controller',
589 'supports' => array(
590 'title',
591 'editor',
592 'revisions',
593 ),
594 )
595 );
596
597 register_post_type(
598 'wp_font_family',
599 array(
600 'labels' => array(
601 'name' => __( 'Font Families' ),
602 'singular_name' => __( 'Font Family' ),
603 ),
604 'public' => false,
605 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
606 'hierarchical' => false,
607 'capabilities' => array(
608 'read' => 'edit_theme_options',
609 'read_private_posts' => 'edit_theme_options',
610 'create_posts' => 'edit_theme_options',
611 'publish_posts' => 'edit_theme_options',
612 'edit_posts' => 'edit_theme_options',
613 'edit_others_posts' => 'edit_theme_options',
614 'edit_published_posts' => 'edit_theme_options',
615 'delete_posts' => 'edit_theme_options',
616 'delete_others_posts' => 'edit_theme_options',
617 'delete_published_posts' => 'edit_theme_options',
618 ),
619 'map_meta_cap' => true,
620 'query_var' => false,
621 'rewrite' => false,
622 'show_in_rest' => true,
623 'rest_base' => 'font-families',
624 'rest_controller_class' => 'WP_REST_Font_Families_Controller',
625 'supports' => array( 'title' ),
626 )
627 );
628
629 register_post_type(
630 'wp_font_face',
631 array(
632 'labels' => array(
633 'name' => __( 'Font Faces' ),
634 'singular_name' => __( 'Font Face' ),
635 ),
636 'public' => false,
637 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
638 'hierarchical' => false,
639 'capabilities' => array(
640 'read' => 'edit_theme_options',
641 'read_private_posts' => 'edit_theme_options',
642 'create_posts' => 'edit_theme_options',
643 'publish_posts' => 'edit_theme_options',
644 'edit_posts' => 'edit_theme_options',
645 'edit_others_posts' => 'edit_theme_options',
646 'edit_published_posts' => 'edit_theme_options',
647 'delete_posts' => 'edit_theme_options',
648 'delete_others_posts' => 'edit_theme_options',
649 'delete_published_posts' => 'edit_theme_options',
650 ),
651 'map_meta_cap' => true,
652 'query_var' => false,
653 'rewrite' => false,
654 'show_in_rest' => true,
655 'rest_base' => 'font-families/(?P<font_family_id>[\d]+)/font-faces',
656 'rest_controller_class' => 'WP_REST_Font_Faces_Controller',
657 'supports' => array( 'title' ),
658 )
659 );
660
661 register_post_status(
662 'publish',
663 array(
664 'label' => _x( 'Published', 'post status' ),
665 'public' => true,
666 '_builtin' => true, /* internal use only. */
667 /* translators: %s: Number of published posts. */
668 'label_count' => _n_noop(
669 'Published <span class="count">(%s)</span>',
670 'Published <span class="count">(%s)</span>'
671 ),
672 )
673 );
674
675 register_post_status(
676 'future',
677 array(
678 'label' => _x( 'Scheduled', 'post status' ),
679 'protected' => true,
680 '_builtin' => true, /* internal use only. */
681 /* translators: %s: Number of scheduled posts. */
682 'label_count' => _n_noop(
683 'Scheduled <span class="count">(%s)</span>',
684 'Scheduled <span class="count">(%s)</span>'
685 ),
686 )
687 );
688
689 register_post_status(
690 'draft',
691 array(
692 'label' => _x( 'Draft', 'post status' ),
693 'protected' => true,
694 '_builtin' => true, /* internal use only. */
695 /* translators: %s: Number of draft posts. */
696 'label_count' => _n_noop(
697 'Draft <span class="count">(%s)</span>',
698 'Drafts <span class="count">(%s)</span>'
699 ),
700 'date_floating' => true,
701 )
702 );
703
704 register_post_status(
705 'pending',
706 array(
707 'label' => _x( 'Pending', 'post status' ),
708 'protected' => true,
709 '_builtin' => true, /* internal use only. */
710 /* translators: %s: Number of pending posts. */
711 'label_count' => _n_noop(
712 'Pending <span class="count">(%s)</span>',
713 'Pending <span class="count">(%s)</span>'
714 ),
715 'date_floating' => true,
716 )
717 );
718
719 register_post_status(
720 'private',
721 array(
722 'label' => _x( 'Private', 'post status' ),
723 'private' => true,
724 '_builtin' => true, /* internal use only. */
725 /* translators: %s: Number of private posts. */
726 'label_count' => _n_noop(
727 'Private <span class="count">(%s)</span>',
728 'Private <span class="count">(%s)</span>'
729 ),
730 )
731 );
732
733 register_post_status(
734 'trash',
735 array(
736 'label' => _x( 'Trash', 'post status' ),
737 'internal' => true,
738 '_builtin' => true, /* internal use only. */
739 /* translators: %s: Number of trashed posts. */
740 'label_count' => _n_noop(
741 'Trash <span class="count">(%s)</span>',
742 'Trash <span class="count">(%s)</span>'
743 ),
744 'show_in_admin_status_list' => true,
745 )
746 );
747
748 register_post_status(
749 'auto-draft',
750 array(
751 'label' => 'auto-draft',
752 'internal' => true,
753 '_builtin' => true, /* internal use only. */
754 'date_floating' => true,
755 )
756 );
757
758 register_post_status(
759 'inherit',
760 array(
761 'label' => 'inherit',
762 'internal' => true,
763 '_builtin' => true, /* internal use only. */
764 'exclude_from_search' => false,
765 )
766 );
767
768 register_post_status(
769 'request-pending',
770 array(
771 'label' => _x( 'Pending', 'request status' ),
772 'internal' => true,
773 '_builtin' => true, /* internal use only. */
774 /* translators: %s: Number of pending requests. */
775 'label_count' => _n_noop(
776 'Pending <span class="count">(%s)</span>',
777 'Pending <span class="count">(%s)</span>'
778 ),
779 'exclude_from_search' => false,
780 )
781 );
782
783 register_post_status(
784 'request-confirmed',
785 array(
786 'label' => _x( 'Confirmed', 'request status' ),
787 'internal' => true,
788 '_builtin' => true, /* internal use only. */
789 /* translators: %s: Number of confirmed requests. */
790 'label_count' => _n_noop(
791 'Confirmed <span class="count">(%s)</span>',
792 'Confirmed <span class="count">(%s)</span>'
793 ),
794 'exclude_from_search' => false,
795 )
796 );
797
798 register_post_status(
799 'request-failed',
800 array(
801 'label' => _x( 'Failed', 'request status' ),
802 'internal' => true,
803 '_builtin' => true, /* internal use only. */
804 /* translators: %s: Number of failed requests. */
805 'label_count' => _n_noop(
806 'Failed <span class="count">(%s)</span>',
807 'Failed <span class="count">(%s)</span>'
808 ),
809 'exclude_from_search' => false,
810 )
811 );
812
813 register_post_status(
814 'request-completed',
815 array(
816 'label' => _x( 'Completed', 'request status' ),
817 'internal' => true,
818 '_builtin' => true, /* internal use only. */
819 /* translators: %s: Number of completed requests. */
820 'label_count' => _n_noop(
821 'Completed <span class="count">(%s)</span>',
822 'Completed <span class="count">(%s)</span>'
823 ),
824 'exclude_from_search' => false,
825 )
826 );
827}
828
829/**
830 * Retrieves attached file path based on attachment ID.
831 *
832 * By default the path will go through the {@see 'get_attached_file'} filter, but
833 * passing `true` to the `$unfiltered` argument will return the file path unfiltered.
834 *
835 * The function works by retrieving the `_wp_attached_file` post meta value.
836 * This is a convenience function to prevent looking up the meta name and provide
837 * a mechanism for sending the attached filename through a filter.
838 *
839 * @since 2.0.0
840 *
841 * @param int $attachment_id Attachment ID.
842 * @param bool $unfiltered Optional. Whether to skip the {@see 'get_attached_file'} filter.
843 * Default false.
844 * @return string|false The file path to where the attached file should be, false otherwise.
845 */
846function get_attached_file( $attachment_id, $unfiltered = false ) {
847 $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
848
849 // If the file is relative, prepend upload dir.
850 if ( $file && ! str_starts_with( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) ) {
851 $uploads = wp_get_upload_dir();
852 if ( false === $uploads['error'] ) {
853 $file = $uploads['basedir'] . "/$file";
854 }
855 }
856
857 if ( $unfiltered ) {
858 return $file;
859 }
860
861 /**
862 * Filters the attached file based on the given ID.
863 *
864 * @since 2.1.0
865 *
866 * @param string|false $file The file path to where the attached file should be, false otherwise.
867 * @param int $attachment_id Attachment ID.
868 */
869 return apply_filters( 'get_attached_file', $file, $attachment_id );
870}
871
872/**
873 * Updates attachment file path based on attachment ID.
874 *
875 * Used to update the file path of the attachment, which uses post meta name
876 * `_wp_attached_file` to store the path of the attachment.
877 *
878 * @since 2.1.0
879 *
880 * @param int $attachment_id Attachment ID.
881 * @param string $file File path for the attachment.
882 * @return int|bool Meta ID if the `_wp_attached_file` key didn't exist for the attachment.
883 * True on successful update, false on failure or if the `$file` value passed
884 * to the function is the same as the one that is already in the database.
885 */
886function update_attached_file( $attachment_id, $file ) {
887 if ( ! get_post( $attachment_id ) ) {
888 return false;
889 }
890
891 /**
892 * Filters the path to the attached file to update.
893 *
894 * @since 2.1.0
895 *
896 * @param string $file Path to the attached file to update.
897 * @param int $attachment_id Attachment ID.
898 */
899 $file = apply_filters( 'update_attached_file', $file, $attachment_id );
900
901 $file = _wp_relative_upload_path( $file );
902 if ( $file ) {
903 return update_post_meta( $attachment_id, '_wp_attached_file', $file );
904 } else {
905 return delete_post_meta( $attachment_id, '_wp_attached_file' );
906 }
907}
908
909/**
910 * Returns relative path to an uploaded file.
911 *
912 * The path is relative to the current upload dir.
913 *
914 * @since 2.9.0
915 * @access private
916 *
917 * @param string $path Full path to the file.
918 * @return string Relative path on success, unchanged path on failure.
919 */
920function _wp_relative_upload_path( $path ) {
921 $new_path = $path;
922
923 $uploads = wp_get_upload_dir();
924 if ( str_starts_with( $new_path, $uploads['basedir'] ) ) {
925 $new_path = str_replace( $uploads['basedir'], '', $new_path );
926 $new_path = ltrim( $new_path, '/' );
927 }
928
929 /**
930 * Filters the relative path to an uploaded file.
931 *
932 * @since 2.9.0
933 *
934 * @param string $new_path Relative path to the file.
935 * @param string $path Full path to the file.
936 */
937 return apply_filters( '_wp_relative_upload_path', $new_path, $path );
938}
939
940/**
941 * Retrieves all children of the post parent ID.
942 *
943 * Normally, without any enhancements, the children would apply to pages. In the
944 * context of the inner workings of WordPress, pages, posts, and attachments
945 * share the same table, so therefore the functionality could apply to any one
946 * of them. It is then noted that while this function does not work on posts, it
947 * does not mean that it won't work on posts. It is recommended that you know
948 * what context you wish to retrieve the children of.
949 *
950 * Attachments may also be made the child of a post, so if that is an accurate
951 * statement (which needs to be verified), it would then be possible to get
952 * all of the attachments for a post. Attachments have since changed since
953 * version 2.5, so this is most likely inaccurate, but serves generally as an
954 * example of what is possible.
955 *
956 * The arguments listed as defaults are for this function and also of the
957 * get_posts() function. The arguments are combined with the get_children defaults
958 * and are then passed to the get_posts() function, which accepts additional arguments.
959 * You can replace the defaults in this function, listed below and the additional
960 * arguments listed in the get_posts() function.
961 *
962 * The 'post_parent' is the most important argument and important attention
963 * needs to be paid to the $args parameter. If you pass either an object or an
964 * integer (number), then just the 'post_parent' is grabbed and everything else
965 * is lost. If you don't specify any arguments, then it is assumed that you are
966 * in The Loop and the post parent will be grabbed for from the current post.
967 *
968 * The 'post_parent' argument is the ID to get the children. The 'numberposts'
969 * is the amount of posts to retrieve that has a default of '-1', which is
970 * used to get all of the posts. Giving a number higher than 0 will only
971 * retrieve that amount of posts.
972 *
973 * The 'post_type' and 'post_status' arguments can be used to choose what
974 * criteria of posts to retrieve. The 'post_type' can be anything, but WordPress
975 * post types are 'post', 'pages', and 'attachments'. The 'post_status'
976 * argument will accept any post status within the write administration panels.
977 *
978 * @since 2.0.0
979 *
980 * @see get_posts()
981 * @todo Check validity of description.
982 *
983 * @global WP_Post $post Global post object.
984 *
985 * @param mixed $args Optional. User defined arguments for replacing the defaults. Default empty.
986 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
987 * correspond to a WP_Post object, an associative array, or a numeric array,
988 * respectively. Default OBJECT.
989 * @return WP_Post[]|array[]|int[] Array of post objects, arrays, or IDs, depending on `$output`.
990 */
991function get_children( $args = '', $output = OBJECT ) {
992 $kids = array();
993 if ( empty( $args ) ) {
994 if ( isset( $GLOBALS['post'] ) ) {
995 $args = array( 'post_parent' => (int) $GLOBALS['post']->post_parent );
996 } else {
997 return $kids;
998 }
999 } elseif ( is_object( $args ) ) {
1000 $args = array( 'post_parent' => (int) $args->post_parent );
1001 } elseif ( is_numeric( $args ) ) {
1002 $args = array( 'post_parent' => (int) $args );
1003 }
1004
1005 $defaults = array(
1006 'numberposts' => -1,
1007 'post_type' => 'any',
1008 'post_status' => 'any',
1009 'post_parent' => 0,
1010 );
1011
1012 $parsed_args = wp_parse_args( $args, $defaults );
1013
1014 $children = get_posts( $parsed_args );
1015
1016 if ( ! $children ) {
1017 return $kids;
1018 }
1019
1020 if ( ! empty( $parsed_args['fields'] ) ) {
1021 return $children;
1022 }
1023
1024 update_post_cache( $children );
1025
1026 foreach ( $children as $key => $child ) {
1027 $kids[ $child->ID ] = $children[ $key ];
1028 }
1029
1030 if ( OBJECT === $output ) {
1031 return $kids;
1032 } elseif ( ARRAY_A === $output ) {
1033 $weeuns = array();
1034 foreach ( (array) $kids as $kid ) {
1035 $weeuns[ $kid->ID ] = get_object_vars( $kids[ $kid->ID ] );
1036 }
1037 return $weeuns;
1038 } elseif ( ARRAY_N === $output ) {
1039 $babes = array();
1040 foreach ( (array) $kids as $kid ) {
1041 $babes[ $kid->ID ] = array_values( get_object_vars( $kids[ $kid->ID ] ) );
1042 }
1043 return $babes;
1044 } else {
1045 return $kids;
1046 }
1047}
1048
1049/**
1050 * Gets extended entry info (<!--more-->).
1051 *
1052 * There should not be any space after the second dash and before the word
1053 * 'more'. There can be text or space(s) after the word 'more', but won't be
1054 * referenced.
1055 *
1056 * The returned array has 'main', 'extended', and 'more_text' keys. Main has the text before
1057 * the `<!--more-->`. The 'extended' key has the content after the
1058 * `<!--more-->` comment. The 'more_text' key has the custom "Read More" text.
1059 *
1060 * @since 1.0.0
1061 *
1062 * @param string $post Post content.
1063 * @return string[] {
1064 * Extended entry info.
1065 *
1066 * @type string $main Content before the more tag.
1067 * @type string $extended Content after the more tag.
1068 * @type string $more_text Custom read more text, or empty string.
1069 * }
1070 */
1071function get_extended( $post ) {
1072 // Match the new style more links.
1073 if ( preg_match( '/<!--more(.*?)?-->/', $post, $matches ) ) {
1074 list($main, $extended) = explode( $matches[0], $post, 2 );
1075 $more_text = $matches[1];
1076 } else {
1077 $main = $post;
1078 $extended = '';
1079 $more_text = '';
1080 }
1081
1082 // Leading and trailing whitespace.
1083 $main = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $main );
1084 $extended = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $extended );
1085 $more_text = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $more_text );
1086
1087 return array(
1088 'main' => $main,
1089 'extended' => $extended,
1090 'more_text' => $more_text,
1091 );
1092}
1093
1094/**
1095 * Retrieves post data given a post ID or post object.
1096 *
1097 * See sanitize_post() for optional $filter values. Also, the parameter
1098 * `$post`, must be given as a variable, since it is passed by reference.
1099 *
1100 * @since 1.5.1
1101 *
1102 * @global WP_Post $post Global post object.
1103 *
1104 * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values
1105 * return the current global post inside the loop. A numerically valid post ID that
1106 * points to a non-existent post returns `null`. Defaults to global $post.
1107 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
1108 * correspond to a WP_Post object, an associative array, or a numeric array,
1109 * respectively. Default OBJECT.
1110 * @param string $filter Optional. Type of filter to apply. Accepts 'raw', 'edit', 'db',
1111 * or 'display'. Default 'raw'.
1112 * @return WP_Post|array|null Type corresponding to $output on success or null on failure.
1113 * When $output is OBJECT, a `WP_Post` instance is returned.
1114 */
1115function get_post( $post = null, $output = OBJECT, $filter = 'raw' ) {
1116 if ( empty( $post ) && isset( $GLOBALS['post'] ) ) {
1117 $post = $GLOBALS['post'];
1118 }
1119
1120 if ( $post instanceof WP_Post ) {
1121 $_post = $post;
1122 } elseif ( is_object( $post ) ) {
1123 if ( empty( $post->filter ) ) {
1124 $_post = sanitize_post( $post, 'raw' );
1125 $_post = new WP_Post( $_post );
1126 } elseif ( 'raw' === $post->filter ) {
1127 $_post = new WP_Post( $post );
1128 } else {
1129 $_post = WP_Post::get_instance( $post->ID );
1130 }
1131 } else {
1132 $_post = WP_Post::get_instance( $post );
1133 }
1134
1135 if ( ! $_post ) {
1136 return null;
1137 }
1138
1139 $_post = $_post->filter( $filter );
1140
1141 if ( ARRAY_A === $output ) {
1142 return $_post->to_array();
1143 } elseif ( ARRAY_N === $output ) {
1144 return array_values( $_post->to_array() );
1145 }
1146
1147 return $_post;
1148}
1149
1150/**
1151 * Retrieves the IDs of the ancestors of a post.
1152 *
1153 * @since 2.5.0
1154 *
1155 * @param int|WP_Post $post Post ID or post object.
1156 * @return int[] Array of ancestor IDs or empty array if there are none.
1157 */
1158function get_post_ancestors( $post ) {
1159 $post = get_post( $post );
1160
1161 if ( ! $post || empty( $post->post_parent ) || $post->post_parent === $post->ID ) {
1162 return array();
1163 }
1164
1165 $ancestors = array();
1166
1167 $id = $post->post_parent;
1168 $ancestors[] = $id;
1169
1170 while ( $ancestor = get_post( $id ) ) {
1171 // Loop detection: If the ancestor has been seen before, break.
1172 if ( empty( $ancestor->post_parent ) || $ancestor->post_parent === $post->ID
1173 || in_array( $ancestor->post_parent, $ancestors, true )
1174 ) {
1175 break;
1176 }
1177
1178 $id = $ancestor->post_parent;
1179 $ancestors[] = $id;
1180 }
1181
1182 return $ancestors;
1183}
1184
1185/**
1186 * Retrieves data from a post field based on Post ID.
1187 *
1188 * Examples of the post field will be, 'post_type', 'post_status', 'post_content',
1189 * etc and based off of the post object property or key names.
1190 *
1191 * The context values are based off of the taxonomy filter functions and
1192 * supported values are found within those functions.
1193 *
1194 * @since 2.3.0
1195 * @since 4.5.0 The `$post` parameter was made optional.
1196 *
1197 * @see sanitize_post_field()
1198 *
1199 * @param string $field Post field name.
1200 * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
1201 * @param string $context Optional. How to filter the field. Accepts 'raw', 'edit', 'db',
1202 * or 'display'. Default 'display'.
1203 * @return int|string|int[] The value of the post field on success, empty string on failure.
1204 */
1205function get_post_field( $field, $post = null, $context = 'display' ) {
1206 $post = get_post( $post );
1207
1208 if ( ! $post ) {
1209 return '';
1210 }
1211
1212 if ( ! isset( $post->$field ) ) {
1213 return '';
1214 }
1215
1216 return sanitize_post_field( $field, $post->$field, $post->ID, $context );
1217}
1218
1219/**
1220 * Retrieves the mime type of an attachment based on the ID.
1221 *
1222 * This function can be used with any post type, but it makes more sense with
1223 * attachments.
1224 *
1225 * @since 2.0.0
1226 *
1227 * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
1228 * @return string|false The mime type on success, false on failure.
1229 */
1230function get_post_mime_type( $post = null ) {
1231 $post = get_post( $post );
1232
1233 if ( is_object( $post ) ) {
1234 return $post->post_mime_type;
1235 }
1236
1237 return false;
1238}
1239
1240/**
1241 * Retrieves the post status based on the post ID.
1242 *
1243 * If the post ID is of an attachment, then the parent post status will be given
1244 * instead.
1245 *
1246 * @since 2.0.0
1247 *
1248 * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post.
1249 * @return string|false Post status on success, false on failure.
1250 */
1251function get_post_status( $post = null ) {
1252 // Normalize the post object if necessary, skip normalization if called from get_sample_permalink().
1253 if ( ! $post instanceof WP_Post || ! isset( $post->filter ) || 'sample' !== $post->filter ) {
1254 $post = get_post( $post );
1255 }
1256
1257 if ( ! is_object( $post ) ) {
1258 return false;
1259 }
1260
1261 $post_status = $post->post_status;
1262
1263 if (
1264 'attachment' === $post->post_type &&
1265 'inherit' === $post_status
1266 ) {
1267 if (
1268 0 === $post->post_parent ||
1269 ! get_post( $post->post_parent ) ||
1270 $post->ID === $post->post_parent
1271 ) {
1272 // Unattached attachments with inherit status are assumed to be published.
1273 $post_status = 'publish';
1274 } elseif ( 'trash' === get_post_status( $post->post_parent ) ) {
1275 // Get parent status prior to trashing.
1276 $post_status = get_post_meta( $post->post_parent, '_wp_trash_meta_status', true );
1277
1278 if ( ! $post_status ) {
1279 // Assume publish as above.
1280 $post_status = 'publish';
1281 }
1282 } else {
1283 $post_status = get_post_status( $post->post_parent );
1284 }
1285 } elseif (
1286 'attachment' === $post->post_type &&
1287 ! in_array( $post_status, array( 'private', 'trash', 'auto-draft' ), true )
1288 ) {
1289 /*
1290 * Ensure uninherited attachments have a permitted status either 'private', 'trash', 'auto-draft'.
1291 * This is to match the logic in wp_insert_post().
1292 *
1293 * Note: 'inherit' is excluded from this check as it is resolved to the parent post's
1294 * status in the logic block above.
1295 */
1296 $post_status = 'publish';
1297 }
1298
1299 /**
1300 * Filters the post status.
1301 *
1302 * @since 4.4.0
1303 * @since 5.7.0 The attachment post type is now passed through this filter.
1304 *
1305 * @param string $post_status The post status.
1306 * @param WP_Post $post The post object.
1307 */
1308 return apply_filters( 'get_post_status', $post_status, $post );
1309}
1310
1311/**
1312 * Retrieves all of the WordPress supported post statuses.
1313 *
1314 * Posts have a limited set of valid status values, this provides the
1315 * post_status values and descriptions.
1316 *
1317 * @since 2.5.0
1318 *
1319 * @return string[] Array of post status labels keyed by their status.
1320 */
1321function get_post_statuses() {
1322 $status = array(
1323 'draft' => __( 'Draft' ),
1324 'pending' => __( 'Pending Review' ),
1325 'private' => __( 'Private' ),
1326 'publish' => __( 'Published' ),
1327 );
1328
1329 return $status;
1330}
1331
1332/**
1333 * Retrieves all of the WordPress support page statuses.
1334 *
1335 * Pages have a limited set of valid status values, this provides the
1336 * post_status values and descriptions.
1337 *
1338 * @since 2.5.0
1339 *
1340 * @return string[] Array of page status labels keyed by their status.
1341 */
1342function get_page_statuses() {
1343 $status = array(
1344 'draft' => __( 'Draft' ),
1345 'private' => __( 'Private' ),
1346 'publish' => __( 'Published' ),
1347 );
1348
1349 return $status;
1350}
1351
1352/**
1353 * Returns statuses for privacy requests.
1354 *
1355 * @since 4.9.6
1356 * @access private
1357 *
1358 * @return string[] Array of privacy request status labels keyed by their status.
1359 */
1360function _wp_privacy_statuses() {
1361 return array(
1362 'request-pending' => _x( 'Pending', 'request status' ), // Pending confirmation from user.
1363 'request-confirmed' => _x( 'Confirmed', 'request status' ), // User has confirmed the action.
1364 'request-failed' => _x( 'Failed', 'request status' ), // User failed to confirm the action.
1365 'request-completed' => _x( 'Completed', 'request status' ), // Admin has handled the request.
1366 );
1367}
1368
1369/**
1370 * Registers a post status. Do not use before init.
1371 *
1372 * A simple function for creating or modifying a post status based on the
1373 * parameters given. The function will accept an array (second optional
1374 * parameter), along with a string for the post status name.
1375 *
1376 * Arguments prefixed with an _underscore shouldn't be used by plugins and themes.
1377 *
1378 * @since 3.0.0
1379 *
1380 * @global stdClass[] $wp_post_statuses Inserts new post status object into the list
1381 *
1382 * @param string $post_status Name of the post status.
1383 * @param array|string $args {
1384 * Optional. Array or string of post status arguments.
1385 *
1386 * @type bool|string $label A descriptive name for the post status marked
1387 * for translation. Defaults to value of $post_status.
1388 * @type array|false $label_count Nooped plural text from _n_noop() to provide the singular
1389 * and plural forms of the label for counts. Default false
1390 * which means the `$label` argument will be used for both
1391 * the singular and plural forms of this label.
1392 * @type bool $exclude_from_search Whether to exclude posts with this post status
1393 * from search results. Default is value of $internal.
1394 * @type bool $_builtin Whether the status is built-in. Core-use only.
1395 * Default false.
1396 * @type bool $public Whether posts of this status should be shown
1397 * in the front end of the site. Default false.
1398 * @type bool $internal Whether the status is for internal use only.
1399 * Default false.
1400 * @type bool $protected Whether posts with this status should be protected.
1401 * Default false.
1402 * @type bool $private Whether posts with this status should be private.
1403 * Default false.
1404 * @type bool $publicly_queryable Whether posts with this status should be publicly-
1405 * queryable. Default is value of $public.
1406 * @type bool $show_in_admin_all_list Whether to include posts in the edit listing for
1407 * their post type. Default is the opposite value
1408 * of $internal.
1409 * @type bool $show_in_admin_status_list Show in the list of statuses with post counts at
1410 * the top of the edit listings,
1411 * e.g. All (12) | Published (9) | My Custom Status (2)
1412 * Default is the opposite value of $internal.
1413 * @type bool $date_floating Whether the post has a floating creation date.
1414 * Default to false.
1415 * }
1416 * @return object
1417 */
1418function register_post_status( $post_status, $args = array() ) {
1419 global $wp_post_statuses;
1420
1421 if ( ! is_array( $wp_post_statuses ) ) {
1422 $wp_post_statuses = array();
1423 }
1424
1425 // Args prefixed with an underscore are reserved for internal use.
1426 $defaults = array(
1427 'label' => false,
1428 'label_count' => false,
1429 'exclude_from_search' => null,
1430 '_builtin' => false,
1431 'public' => null,
1432 'internal' => null,
1433 'protected' => null,
1434 'private' => null,
1435 'publicly_queryable' => null,
1436 'show_in_admin_status_list' => null,
1437 'show_in_admin_all_list' => null,
1438 'date_floating' => null,
1439 );
1440 $args = wp_parse_args( $args, $defaults );
1441 $args = (object) $args;
1442
1443 $post_status = sanitize_key( $post_status );
1444 $args->name = $post_status;
1445
1446 // Set various defaults.
1447 if ( null === $args->public && null === $args->internal && null === $args->protected && null === $args->private ) {
1448 $args->internal = true;
1449 }
1450
1451 if ( null === $args->public ) {
1452 $args->public = false;
1453 }
1454
1455 if ( null === $args->private ) {
1456 $args->private = false;
1457 }
1458
1459 if ( null === $args->protected ) {
1460 $args->protected = false;
1461 }
1462
1463 if ( null === $args->internal ) {
1464 $args->internal = false;
1465 }
1466
1467 if ( null === $args->publicly_queryable ) {
1468 $args->publicly_queryable = $args->public;
1469 }
1470
1471 if ( null === $args->exclude_from_search ) {
1472 $args->exclude_from_search = $args->internal;
1473 }
1474
1475 if ( null === $args->show_in_admin_all_list ) {
1476 $args->show_in_admin_all_list = ! $args->internal;
1477 }
1478
1479 if ( null === $args->show_in_admin_status_list ) {
1480 $args->show_in_admin_status_list = ! $args->internal;
1481 }
1482
1483 if ( null === $args->date_floating ) {
1484 $args->date_floating = false;
1485 }
1486
1487 if ( false === $args->label ) {
1488 $args->label = $post_status;
1489 }
1490
1491 if ( false === $args->label_count ) {
1492 // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralSingular,WordPress.WP.I18n.NonSingularStringLiteralPlural
1493 $args->label_count = _n_noop( $args->label, $args->label );
1494 }
1495
1496 $wp_post_statuses[ $post_status ] = $args;
1497
1498 return $args;
1499}
1500
1501/**
1502 * Retrieves a post status object by name.
1503 *
1504 * @since 3.0.0
1505 *
1506 * @see register_post_status()
1507 *
1508 * @global stdClass[] $wp_post_statuses List of post statuses.
1509 *
1510 * @param string $post_status The name of a registered post status.
1511 * @return stdClass|null A post status object.
1512 */
1513function get_post_status_object( $post_status ) {
1514 global $wp_post_statuses;
1515
1516 if ( ! is_string( $post_status ) || empty( $wp_post_statuses[ $post_status ] ) ) {
1517 return null;
1518 }
1519
1520 return $wp_post_statuses[ $post_status ];
1521}
1522
1523/**
1524 * Gets a list of post statuses.
1525 *
1526 * @since 3.0.0
1527 *
1528 * @see register_post_status()
1529 *
1530 * @global stdClass[] $wp_post_statuses List of post statuses.
1531 *
1532 * @param array|string $args Optional. Array or string of post status arguments to compare against
1533 * properties of the global `$wp_post_statuses objects`. Default empty array.
1534 * @param string $output Optional. The type of output to return, either 'names' or 'objects'. Default 'names'.
1535 * @param string $operator Optional. The logical operation to perform. 'or' means only one element
1536 * from the array needs to match; 'and' means all elements must match.
1537 * Default 'and'.
1538 * @return string[]|stdClass[] A list of post status names or objects.
1539 */
1540function get_post_stati( $args = array(), $output = 'names', $operator = 'and' ) {
1541 global $wp_post_statuses;
1542
1543 $field = ( 'names' === $output ) ? 'name' : false;
1544
1545 return wp_filter_object_list( $wp_post_statuses, $args, $operator, $field );
1546}
1547
1548/**
1549 * Determines whether the post type is hierarchical.
1550 *
1551 * A false return value might also mean that the post type does not exist.
1552 *
1553 * @since 3.0.0
1554 *
1555 * @see get_post_type_object()
1556 *
1557 * @param string $post_type Post type name
1558 * @return bool Whether post type is hierarchical.
1559 */
1560function is_post_type_hierarchical( $post_type ) {
1561 if ( ! post_type_exists( $post_type ) ) {
1562 return false;
1563 }
1564
1565 $post_type = get_post_type_object( $post_type );
1566 return $post_type->hierarchical;
1567}
1568
1569/**
1570 * Determines whether a post type is registered.
1571 *
1572 * For more information on this and similar theme functions, check out
1573 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
1574 * Conditional Tags} article in the Theme Developer Handbook.
1575 *
1576 * @since 3.0.0
1577 *
1578 * @see get_post_type_object()
1579 *
1580 * @param string $post_type Post type name.
1581 * @return bool Whether post type is registered.
1582 */
1583function post_type_exists( $post_type ) {
1584 return (bool) get_post_type_object( $post_type );
1585}
1586
1587/**
1588 * Retrieves the post type of the current post or of a given post.
1589 *
1590 * @since 2.1.0
1591 *
1592 * @param int|WP_Post|null $post Optional. Post ID or post object. Default is global $post.
1593 * @return string|false Post type on success, false on failure.
1594 */
1595function get_post_type( $post = null ) {
1596 $post = get_post( $post );
1597 if ( $post ) {
1598 return $post->post_type;
1599 }
1600
1601 return false;
1602}
1603
1604/**
1605 * Retrieves a post type object by name.
1606 *
1607 * @since 3.0.0
1608 * @since 4.6.0 Object returned is now an instance of `WP_Post_Type`.
1609 *
1610 * @see register_post_type()
1611 *
1612 * @global array $wp_post_types List of post types.
1613 *
1614 * @param string $post_type The name of a registered post type.
1615 * @return WP_Post_Type|null WP_Post_Type object if it exists, null otherwise.
1616 */
1617function get_post_type_object( $post_type ) {
1618 global $wp_post_types;
1619
1620 if ( ! is_scalar( $post_type ) || empty( $wp_post_types[ $post_type ] ) ) {
1621 return null;
1622 }
1623
1624 return $wp_post_types[ $post_type ];
1625}
1626
1627/**
1628 * Gets a list of all registered post type objects.
1629 *
1630 * @since 2.9.0
1631 *
1632 * @see register_post_type() for accepted arguments.
1633 *
1634 * @global array $wp_post_types List of post types.
1635 *
1636 * @param array|string $args Optional. An array of key => value arguments to match against
1637 * the post type objects. Default empty array.
1638 * @param string $output Optional. The type of output to return. Either 'names'
1639 * or 'objects'. Default 'names'.
1640 * @param string $operator Optional. The logical operation to perform. 'or' means only one
1641 * element from the array needs to match; 'and' means all elements
1642 * must match; 'not' means no elements may match. Default 'and'.
1643 * @return string[]|WP_Post_Type[] An array of post type names or objects.
1644 */
1645function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) {
1646 global $wp_post_types;
1647
1648 $field = ( 'names' === $output ) ? 'name' : false;
1649
1650 return wp_filter_object_list( $wp_post_types, $args, $operator, $field );
1651}
1652
1653/**
1654 * Registers a post type.
1655 *
1656 * Note: Post type registrations should not be hooked before the
1657 * {@see 'init'} action. Also, any taxonomy connections should be
1658 * registered via the `$taxonomies` argument to ensure consistency
1659 * when hooks such as {@see 'parse_query'} or {@see 'pre_get_posts'}
1660 * are used.
1661 *
1662 * Post types can support any number of built-in core features such
1663 * as meta boxes, custom fields, post thumbnails, post statuses,
1664 * comments, and more. See the `$supports` argument for a complete
1665 * list of supported features.
1666 *
1667 * @since 2.9.0
1668 * @since 3.0.0 The `show_ui` argument is now enforced on the new post screen.
1669 * @since 4.4.0 The `show_ui` argument is now enforced on the post type listing
1670 * screen and post editing screen.
1671 * @since 4.6.0 Post type object returned is now an instance of `WP_Post_Type`.
1672 * @since 4.7.0 Introduced `show_in_rest`, `rest_base` and `rest_controller_class`
1673 * arguments to register the post type in REST API.
1674 * @since 5.0.0 The `template` and `template_lock` arguments were added.
1675 * @since 5.3.0 The `supports` argument will now accept an array of arguments for a feature.
1676 * @since 5.9.0 The `rest_namespace` argument was added.
1677 *
1678 * @global array $wp_post_types List of post types.
1679 *
1680 * @param string $post_type Post type key. Must not exceed 20 characters and may only contain
1681 * lowercase alphanumeric characters, dashes, and underscores. See sanitize_key().
1682 * @param array|string $args {
1683 * Array or string of arguments for registering a post type.
1684 *
1685 * @type string $label Name of the post type shown in the menu. Usually plural.
1686 * Default is value of $labels['name'].
1687 * @type string[] $labels An array of labels for this post type. If not set, post
1688 * labels are inherited for non-hierarchical types and page
1689 * labels for hierarchical ones. See get_post_type_labels() for a full
1690 * list of supported labels.
1691 * @type string $description A short descriptive summary of what the post type is.
1692 * Default empty.
1693 * @type bool $public Whether a post type is intended for use publicly either via
1694 * the admin interface or by front-end users. While the default
1695 * settings of $exclude_from_search, $publicly_queryable, $show_ui,
1696 * and $show_in_nav_menus are inherited from $public, each does not
1697 * rely on this relationship and controls a very specific intention.
1698 * Default false.
1699 * @type bool $hierarchical Whether the post type is hierarchical (e.g. page). Default false.
1700 * @type bool $exclude_from_search Whether to exclude posts with this post type from front end search
1701 * results. Default is the opposite value of $public.
1702 * @type bool $publicly_queryable Whether queries can be performed on the front end for the post type
1703 * as part of parse_request(). Endpoints would include:
1704 * * ?post_type={post_type_key}
1705 * * ?{post_type_key}={single_post_slug}
1706 * * ?{post_type_query_var}={single_post_slug}
1707 * If not set, the default is inherited from $public.
1708 * @type bool $show_ui Whether to generate and allow a UI for managing this post type in the
1709 * admin. Default is value of $public.
1710 * @type bool|string $show_in_menu Where to show the post type in the admin menu. To work, $show_ui
1711 * must be true. If true, the post type is shown in its own top level
1712 * menu. If false, no menu is shown. If a string of an existing top
1713 * level menu ('tools.php' or 'edit.php?post_type=page', for example), the
1714 * post type will be placed as a sub-menu of that.
1715 * Default is value of $show_ui.
1716 * @type bool $show_in_nav_menus Makes this post type available for selection in navigation menus.
1717 * Default is value of $public.
1718 * @type bool $show_in_admin_bar Makes this post type available via the admin bar. Default is value
1719 * of $show_in_menu.
1720 * @type bool $show_in_rest Whether to include the post type in the REST API. Set this to true
1721 * for the post type to be available in the block editor.
1722 * @type string $rest_base To change the base URL of REST API route. Default is $post_type.
1723 * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2.
1724 * @type string $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'.
1725 * @type string|bool $autosave_rest_controller_class REST API controller class name. Default is 'WP_REST_Autosaves_Controller'.
1726 * @type string|bool $revisions_rest_controller_class REST API controller class name. Default is 'WP_REST_Revisions_Controller'.
1727 * @type bool $late_route_registration A flag to direct the REST API controllers for autosave / revisions
1728 * should be registered before/after the post type controller.
1729 * @type int $menu_position The position in the menu order the post type should appear. To work,
1730 * $show_in_menu must be true. Default null (at the bottom).
1731 * @type string $menu_icon The URL to the icon to be used for this menu. Pass a base64-encoded
1732 * SVG using a data URI, which will be colored to match the color scheme
1733 * -- this should begin with 'data:image/svg+xml;base64,'. Pass the name
1734 * of a Dashicons helper class to use a font icon, e.g.
1735 * 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty
1736 * so an icon can be added via CSS. Defaults to use the posts icon.
1737 * @type string|array $capability_type The string to use to build the read, edit, and delete capabilities.
1738 * May be passed as an array to allow for alternative plurals when using
1739 * this argument as a base to construct the capabilities, e.g.
1740 * array('story', 'stories'). Default 'post'.
1741 * @type string[] $capabilities Array of capabilities for this post type. $capability_type is used
1742 * as a base to construct capabilities by default.
1743 * See get_post_type_capabilities().
1744 * @type bool $map_meta_cap Whether to use the internal default meta capability handling.
1745 * Default false.
1746 * @type array|false $supports Core feature(s) the post type supports. Serves as an alias for calling
1747 * add_post_type_support() directly. Core features include 'title',
1748 * 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt',
1749 * 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'.
1750 * Additionally, the 'revisions' feature dictates whether the post type
1751 * will store revisions, the 'autosave' feature dictates whether the post type
1752 * will be autosaved, and the 'comments' feature dictates whether the
1753 * comments count will show on the edit screen. For backward compatibility reasons,
1754 * adding 'editor' support implies 'autosave' support too. A feature can also be
1755 * specified as an array of arguments to provide additional information
1756 * about supporting that feature.
1757 * Example: `array( 'my_feature', array( 'field' => 'value' ) )`.
1758 * If false, no features will be added.
1759 * Default is an array containing 'title' and 'editor'.
1760 * @type callable $register_meta_box_cb Provide a callback function that sets up the meta boxes for the
1761 * edit form. Do remove_meta_box() and add_meta_box() calls in the
1762 * callback. Default null.
1763 * @type string[] $taxonomies An array of taxonomy identifiers that will be registered for the
1764 * post type. Taxonomies can be registered later with register_taxonomy()
1765 * or register_taxonomy_for_object_type().
1766 * Default empty array.
1767 * @type bool|string $has_archive Whether there should be post type archives, or if a string, the
1768 * archive slug to use. Will generate the proper rewrite rules if
1769 * $rewrite is enabled. Default false.
1770 * @type bool|array $rewrite {
1771 * Triggers the handling of rewrites for this post type. To prevent rewrite, set to false.
1772 * Defaults to true, using $post_type as slug. To specify rewrite rules, an array can be
1773 * passed with any of these keys:
1774 *
1775 * @type string $slug Customize the permastruct slug. Defaults to $post_type key.
1776 * @type bool $with_front Whether the permastruct should be prepended with WP_Rewrite::$front.
1777 * Default true.
1778 * @type bool $feeds Whether the feed permastruct should be built for this post type.
1779 * Default is value of $has_archive.
1780 * @type bool $pages Whether the permastruct should provide for pagination. Default true.
1781 * @type int $ep_mask Endpoint mask to assign. If not specified and permalink_epmask is set,
1782 * inherits from $permalink_epmask. If not specified and permalink_epmask
1783 * is not set, defaults to EP_PERMALINK.
1784 * }
1785 * @type string|bool $query_var Sets the query_var key for this post type. Defaults to $post_type
1786 * key. If false, a post type cannot be loaded at
1787 * ?{query_var}={post_slug}. If specified as a string, the query
1788 * ?{query_var_string}={post_slug} will be valid.
1789 * @type bool $can_export Whether to allow this post type to be exported. Default true.
1790 * @type bool $delete_with_user Whether to delete posts of this type when deleting a user.
1791 * * If true, posts of this type belonging to the user will be moved
1792 * to Trash when the user is deleted.
1793 * * If false, posts of this type belonging to the user will *not*
1794 * be trashed or deleted.
1795 * * If not set (the default), posts are trashed if post type supports
1796 * the 'author' feature. Otherwise posts are not trashed or deleted.
1797 * Default null.
1798 * @type array $template Array of blocks to use as the default initial state for an editor
1799 * session. Each item should be an array containing block name and
1800 * optional attributes. Default empty array.
1801 * @type string|false $template_lock Whether the block template should be locked if $template is set.
1802 * * If set to 'all', the user is unable to insert new blocks,
1803 * move existing blocks and delete blocks.
1804 * * If set to 'insert', the user is able to move existing blocks
1805 * but is unable to insert new blocks and delete blocks.
1806 * Default false.
1807 * @type bool $_builtin FOR INTERNAL USE ONLY! True if this post type is a native or
1808 * "built-in" post_type. Default false.
1809 * @type string $_edit_link FOR INTERNAL USE ONLY! URL segment to use for edit link of
1810 * this post type. Default 'post.php?post=%d'.
1811 * }
1812 * @return WP_Post_Type|WP_Error The registered post type object on success,
1813 * WP_Error object on failure.
1814 */
1815function register_post_type( $post_type, $args = array() ) {
1816 global $wp_post_types;
1817
1818 if ( ! is_array( $wp_post_types ) ) {
1819 $wp_post_types = array();
1820 }
1821
1822 // Sanitize post type name.
1823 $post_type = sanitize_key( $post_type );
1824
1825 if ( empty( $post_type ) || strlen( $post_type ) > 20 ) {
1826 _doing_it_wrong( __FUNCTION__, __( 'Post type names must be between 1 and 20 characters in length.' ), '4.2.0' );
1827 return new WP_Error( 'post_type_length_invalid', __( 'Post type names must be between 1 and 20 characters in length.' ) );
1828 }
1829
1830 $post_type_object = new WP_Post_Type( $post_type, $args );
1831 $post_type_object->add_supports();
1832 $post_type_object->add_rewrite_rules();
1833 $post_type_object->register_meta_boxes();
1834
1835 $wp_post_types[ $post_type ] = $post_type_object;
1836
1837 $post_type_object->add_hooks();
1838 $post_type_object->register_taxonomies();
1839
1840 /**
1841 * Fires after a post type is registered.
1842 *
1843 * @since 3.3.0
1844 * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object.
1845 *
1846 * @param string $post_type Post type.
1847 * @param WP_Post_Type $post_type_object Arguments used to register the post type.
1848 */
1849 do_action( 'registered_post_type', $post_type, $post_type_object );
1850
1851 /**
1852 * Fires after a specific post type is registered.
1853 *
1854 * The dynamic portion of the filter name, `$post_type`, refers to the post type key.
1855 *
1856 * Possible hook names include:
1857 *
1858 * - `registered_post_type_post`
1859 * - `registered_post_type_page`
1860 *
1861 * @since 6.0.0
1862 *
1863 * @param string $post_type Post type.
1864 * @param WP_Post_Type $post_type_object Arguments used to register the post type.
1865 */
1866 do_action( "registered_post_type_{$post_type}", $post_type, $post_type_object );
1867
1868 return $post_type_object;
1869}
1870
1871/**
1872 * Unregisters a post type.
1873 *
1874 * Cannot be used to unregister built-in post types.
1875 *
1876 * @since 4.5.0
1877 *
1878 * @global array $wp_post_types List of post types.
1879 *
1880 * @param string $post_type Post type to unregister.
1881 * @return true|WP_Error True on success, WP_Error on failure or if the post type doesn't exist.
1882 */
1883function unregister_post_type( $post_type ) {
1884 global $wp_post_types;
1885
1886 if ( ! post_type_exists( $post_type ) ) {
1887 return new WP_Error( 'invalid_post_type', __( 'Invalid post type.' ) );
1888 }
1889
1890 $post_type_object = get_post_type_object( $post_type );
1891
1892 // Do not allow unregistering internal post types.
1893 if ( $post_type_object->_builtin ) {
1894 return new WP_Error( 'invalid_post_type', __( 'Unregistering a built-in post type is not allowed' ) );
1895 }
1896
1897 $post_type_object->remove_supports();
1898 $post_type_object->remove_rewrite_rules();
1899 $post_type_object->unregister_meta_boxes();
1900 $post_type_object->remove_hooks();
1901 $post_type_object->unregister_taxonomies();
1902
1903 unset( $wp_post_types[ $post_type ] );
1904
1905 /**
1906 * Fires after a post type was unregistered.
1907 *
1908 * @since 4.5.0
1909 *
1910 * @param string $post_type Post type key.
1911 */
1912 do_action( 'unregistered_post_type', $post_type );
1913
1914 return true;
1915}
1916
1917/**
1918 * Builds an object with all post type capabilities out of a post type object
1919 *
1920 * Post type capabilities use the 'capability_type' argument as a base, if the
1921 * capability is not set in the 'capabilities' argument array or if the
1922 * 'capabilities' argument is not supplied.
1923 *
1924 * The capability_type argument can optionally be registered as an array, with
1925 * the first value being singular and the second plural, e.g. array('story, 'stories')
1926 * Otherwise, an 's' will be added to the value for the plural form. After
1927 * registration, capability_type will always be a string of the singular value.
1928 *
1929 * By default, the following keys are accepted as part of the capabilities array:
1930 *
1931 * - edit_post, read_post, and delete_post are meta capabilities, which are then
1932 * generally mapped to corresponding primitive capabilities depending on the
1933 * context, which would be the post being edited/read/deleted and the user or
1934 * role being checked. Thus these capabilities would generally not be granted
1935 * directly to users or roles.
1936 *
1937 * - edit_posts - Controls whether objects of this post type can be edited.
1938 * - edit_others_posts - Controls whether objects of this type owned by other users
1939 * can be edited. If the post type does not support an author, then this will
1940 * behave like edit_posts.
1941 * - delete_posts - Controls whether objects of this post type can be deleted.
1942 * - publish_posts - Controls publishing objects of this post type.
1943 * - read_private_posts - Controls whether private objects can be read.
1944 * - create_posts - Controls whether objects of this post type can be created.
1945 *
1946 * These primitive capabilities are checked in core in various locations.
1947 * There are also six other primitive capabilities which are not referenced
1948 * directly in core, except in map_meta_cap(), which takes the three aforementioned
1949 * meta capabilities and translates them into one or more primitive capabilities
1950 * that must then be checked against the user or role, depending on the context.
1951 *
1952 * - read - Controls whether objects of this post type can be read.
1953 * - delete_private_posts - Controls whether private objects can be deleted.
1954 * - delete_published_posts - Controls whether published objects can be deleted.
1955 * - delete_others_posts - Controls whether objects owned by other users can be
1956 * can be deleted. If the post type does not support an author, then this will
1957 * behave like delete_posts.
1958 * - edit_private_posts - Controls whether private objects can be edited.
1959 * - edit_published_posts - Controls whether published objects can be edited.
1960 *
1961 * These additional capabilities are only used in map_meta_cap(). Thus, they are
1962 * only assigned by default if the post type is registered with the 'map_meta_cap'
1963 * argument set to true (default is false).
1964 *
1965 * @since 3.0.0
1966 * @since 5.4.0 'delete_posts' is included in default capabilities.
1967 *
1968 * @see register_post_type()
1969 * @see map_meta_cap()
1970 *
1971 * @param object $args Post type registration arguments.
1972 * @return object {
1973 * Object with all the capabilities as member variables.
1974 *
1975 * @type string $edit_post Capability to edit a post.
1976 * @type string $read_post Capability to read a post.
1977 * @type string $delete_post Capability to delete a post.
1978 * @type string $edit_posts Capability to edit posts.
1979 * @type string $edit_others_posts Capability to edit others' posts.
1980 * @type string $delete_posts Capability to delete posts.
1981 * @type string $publish_posts Capability to publish posts.
1982 * @type string $read_private_posts Capability to read private posts.
1983 * @type string $create_posts Capability to create posts.
1984 * @type string $read Optional. Capability to read a post.
1985 * @type string $delete_private_posts Optional. Capability to delete private posts.
1986 * @type string $delete_published_posts Optional. Capability to delete published posts.
1987 * @type string $delete_others_posts Optional. Capability to delete others' posts.
1988 * @type string $edit_private_posts Optional. Capability to edit private posts.
1989 * @type string $edit_published_posts Optional. Capability to edit published posts.
1990 * }
1991 */
1992function get_post_type_capabilities( $args ) {
1993 if ( ! is_array( $args->capability_type ) ) {
1994 $args->capability_type = array( $args->capability_type, $args->capability_type . 's' );
1995 }
1996
1997 // Singular base for meta capabilities, plural base for primitive capabilities.
1998 list( $singular_base, $plural_base ) = $args->capability_type;
1999
2000 $default_capabilities = array(
2001 // Meta capabilities.
2002 'edit_post' => 'edit_' . $singular_base,
2003 'read_post' => 'read_' . $singular_base,
2004 'delete_post' => 'delete_' . $singular_base,
2005 // Primitive capabilities used outside of map_meta_cap():
2006 'edit_posts' => 'edit_' . $plural_base,
2007 'edit_others_posts' => 'edit_others_' . $plural_base,
2008 'delete_posts' => 'delete_' . $plural_base,
2009 'publish_posts' => 'publish_' . $plural_base,
2010 'read_private_posts' => 'read_private_' . $plural_base,
2011 );
2012
2013 // Primitive capabilities used within map_meta_cap():
2014 if ( $args->map_meta_cap ) {
2015 $default_capabilities_for_mapping = array(
2016 'read' => 'read',
2017 'delete_private_posts' => 'delete_private_' . $plural_base,
2018 'delete_published_posts' => 'delete_published_' . $plural_base,
2019 'delete_others_posts' => 'delete_others_' . $plural_base,
2020 'edit_private_posts' => 'edit_private_' . $plural_base,
2021 'edit_published_posts' => 'edit_published_' . $plural_base,
2022 );
2023 $default_capabilities = array_merge( $default_capabilities, $default_capabilities_for_mapping );
2024 }
2025
2026 $capabilities = array_merge( $default_capabilities, $args->capabilities );
2027
2028 // Post creation capability simply maps to edit_posts by default:
2029 if ( ! isset( $capabilities['create_posts'] ) ) {
2030 $capabilities['create_posts'] = $capabilities['edit_posts'];
2031 }
2032
2033 // Remember meta capabilities for future reference.
2034 if ( $args->map_meta_cap ) {
2035 _post_type_meta_capabilities( $capabilities );
2036 }
2037
2038 return (object) $capabilities;
2039}
2040
2041/**
2042 * Stores or returns a list of post type meta caps for map_meta_cap().
2043 *
2044 * @since 3.1.0
2045 * @access private
2046 *
2047 * @global array $post_type_meta_caps Used to store meta capabilities.
2048 *
2049 * @param string[] $capabilities Post type meta capabilities.
2050 */
2051function _post_type_meta_capabilities( $capabilities = null ) {
2052 global $post_type_meta_caps;
2053
2054 foreach ( $capabilities as $core => $custom ) {
2055 if ( in_array( $core, array( 'read_post', 'delete_post', 'edit_post' ), true ) ) {
2056 $post_type_meta_caps[ $custom ] = $core;
2057 }
2058 }
2059}
2060
2061/**
2062 * Builds an object with all post type labels out of a post type object.
2063 *
2064 * Accepted keys of the label array in the post type object:
2065 *
2066 * - `name` - General name for the post type, usually plural. The same and overridden
2067 * by `$post_type_object->label`. Default is 'Posts' / 'Pages'.
2068 * - `singular_name` - Name for one object of this post type. Default is 'Post' / 'Page'.
2069 * - `add_new` - Label for adding a new item. Default is 'Add Post' / 'Add Page'.
2070 * - `add_new_item` - Label for adding a new singular item. Default is 'Add Post' / 'Add Page'.
2071 * - `edit_item` - Label for editing a singular item. Default is 'Edit Post' / 'Edit Page'.
2072 * - `new_item` - Label for the new item page title. Default is 'New Post' / 'New Page'.
2073 * - `view_item` - Label for viewing a singular item. Default is 'View Post' / 'View Page'.
2074 * - `view_items` - Label for viewing post type archives. Default is 'View Posts' / 'View Pages'.
2075 * - `search_items` - Label for searching plural items. Default is 'Search Posts' / 'Search Pages'.
2076 * - `not_found` - Label used when no items are found. Default is 'No posts found' / 'No pages found'.
2077 * - `not_found_in_trash` - Label used when no items are in the Trash. Default is 'No posts found in Trash' /
2078 * 'No pages found in Trash'.
2079 * - `parent_item_colon` - Label used to prefix parents of hierarchical items. Not used on non-hierarchical
2080 * post types. Default is 'Parent Page:'.
2081 * - `all_items` - Label to signify all items in a submenu link. Default is 'All Posts' / 'All Pages'.
2082 * - `archives` - Label for archives in nav menus. Default is 'Post Archives' / 'Page Archives'.
2083 * - `attributes` - Label for the attributes meta box. Default is 'Post Attributes' / 'Page Attributes'.
2084 * - `insert_into_item` - Label for the media frame button. Default is 'Insert into post' / 'Insert into page'.
2085 * - `uploaded_to_this_item` - Label for the media frame filter. Default is 'Uploaded to this post' /
2086 * 'Uploaded to this page'.
2087 * - `featured_image` - Label for the featured image meta box title. Default is 'Featured image'.
2088 * - `set_featured_image` - Label for setting the featured image. Default is 'Set featured image'.
2089 * - `remove_featured_image` - Label for removing the featured image. Default is 'Remove featured image'.
2090 * - `use_featured_image` - Label in the media frame for using a featured image. Default is 'Use as featured image'.
2091 * - `menu_name` - Label for the menu name. Default is the same as `name`.
2092 * - `filter_items_list` - Label for the table views hidden heading. Default is 'Filter posts list' /
2093 * 'Filter pages list'.
2094 * - `filter_by_date` - Label for the date filter in list tables. Default is 'Filter by date'.
2095 * - `items_list_navigation` - Label for the table pagination hidden heading. Default is 'Posts list navigation' /
2096 * 'Pages list navigation'.
2097 * - `items_list` - Label for the table hidden heading. Default is 'Posts list' / 'Pages list'.
2098 * - `item_published` - Label used when an item is published. Default is 'Post published.' / 'Page published.'
2099 * - `item_published_privately` - Label used when an item is published with private visibility.
2100 * Default is 'Post published privately.' / 'Page published privately.'
2101 * - `item_reverted_to_draft` - Label used when an item is switched to a draft.
2102 * Default is 'Post reverted to draft.' / 'Page reverted to draft.'
2103 * - `item_trashed` - Label used when an item is moved to Trash. Default is 'Post trashed.' / 'Page trashed.'
2104 * - `item_scheduled` - Label used when an item is scheduled for publishing. Default is 'Post scheduled.' /
2105 * 'Page scheduled.'
2106 * - `item_updated` - Label used when an item is updated. Default is 'Post updated.' / 'Page updated.'
2107 * - `item_link` - Title for a navigation link block variation. Default is 'Post Link' / 'Page Link'.
2108 * - `item_link_description` - Description for a navigation link block variation. Default is 'A link to a post.' /
2109 * 'A link to a page.'
2110 *
2111 * Above, the first default value is for non-hierarchical post types (like posts)
2112 * and the second one is for hierarchical post types (like pages).
2113 *
2114 * Note: To set labels used in post type admin notices, see the {@see 'post_updated_messages'} filter.
2115 *
2116 * @since 3.0.0
2117 * @since 4.3.0 Added the `featured_image`, `set_featured_image`, `remove_featured_image`,
2118 * and `use_featured_image` labels.
2119 * @since 4.4.0 Added the `archives`, `insert_into_item`, `uploaded_to_this_item`, `filter_items_list`,
2120 * `items_list_navigation`, and `items_list` labels.
2121 * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object.
2122 * @since 4.7.0 Added the `view_items` and `attributes` labels.
2123 * @since 5.0.0 Added the `item_published`, `item_published_privately`, `item_reverted_to_draft`,
2124 * `item_scheduled`, and `item_updated` labels.
2125 * @since 5.7.0 Added the `filter_by_date` label.
2126 * @since 5.8.0 Added the `item_link` and `item_link_description` labels.
2127 * @since 6.3.0 Added the `item_trashed` label.
2128 * @since 6.4.0 Changed default values for the `add_new` label to include the type of content.
2129 * This matches `add_new_item` and provides more context for better accessibility.
2130 * @since 6.6.0 Added the `template_name` label.
2131 * @since 6.7.0 Restored pre-6.4.0 defaults for the `add_new` label and updated documentation.
2132 * Updated core usage to reference `add_new_item`.
2133 *
2134 * @access private
2135 *
2136 * @param object|WP_Post_Type $post_type_object Post type object.
2137 * @return object Object with all the labels as member variables.
2138 */
2139function get_post_type_labels( $post_type_object ) {
2140 $nohier_vs_hier_defaults = WP_Post_Type::get_default_labels();
2141
2142 $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name'];
2143
2144 $labels = _get_custom_object_labels( $post_type_object, $nohier_vs_hier_defaults );
2145
2146 if ( ! isset( $post_type_object->labels->template_name ) && isset( $post_type_object->labels->singular_name ) ) {
2147 /* translators: %s: Post type name. */
2148 $labels->template_name = sprintf( __( 'Single item: %s' ), $post_type_object->labels->singular_name );
2149 }
2150
2151 $post_type = $post_type_object->name;
2152
2153 $default_labels = clone $labels;
2154
2155 /**
2156 * Filters the labels of a specific post type.
2157 *
2158 * The dynamic portion of the hook name, `$post_type`, refers to
2159 * the post type slug.
2160 *
2161 * Possible hook names include:
2162 *
2163 * - `post_type_labels_post`
2164 * - `post_type_labels_page`
2165 * - `post_type_labels_attachment`
2166 *
2167 * @since 3.5.0
2168 *
2169 * @see get_post_type_labels() for the full list of labels.
2170 *
2171 * @param object $labels Object with labels for the post type as member variables.
2172 */
2173 $labels = apply_filters( "post_type_labels_{$post_type}", $labels );
2174
2175 // Ensure that the filtered labels contain all required default values.
2176 $labels = (object) array_merge( (array) $default_labels, (array) $labels );
2177
2178 return $labels;
2179}
2180
2181/**
2182 * Builds an object with custom-something object (post type, taxonomy) labels
2183 * out of a custom-something object
2184 *
2185 * @since 3.0.0
2186 * @access private
2187 *
2188 * @param object $data_object A custom-something object.
2189 * @param array $nohier_vs_hier_defaults Hierarchical vs non-hierarchical default labels.
2190 * @return object Object containing labels for the given custom-something object.
2191 */
2192function _get_custom_object_labels( $data_object, $nohier_vs_hier_defaults ) {
2193 $data_object->labels = (array) $data_object->labels;
2194
2195 if ( isset( $data_object->label ) && empty( $data_object->labels['name'] ) ) {
2196 $data_object->labels['name'] = $data_object->label;
2197 }
2198
2199 if ( ! isset( $data_object->labels['singular_name'] ) && isset( $data_object->labels['name'] ) ) {
2200 $data_object->labels['singular_name'] = $data_object->labels['name'];
2201 }
2202
2203 if ( ! isset( $data_object->labels['name_admin_bar'] ) ) {
2204 $data_object->labels['name_admin_bar'] =
2205 isset( $data_object->labels['singular_name'] )
2206 ? $data_object->labels['singular_name']
2207 : $data_object->name;
2208 }
2209
2210 if ( ! isset( $data_object->labels['menu_name'] ) && isset( $data_object->labels['name'] ) ) {
2211 $data_object->labels['menu_name'] = $data_object->labels['name'];
2212 }
2213
2214 if ( ! isset( $data_object->labels['all_items'] ) && isset( $data_object->labels['menu_name'] ) ) {
2215 $data_object->labels['all_items'] = $data_object->labels['menu_name'];
2216 }
2217
2218 if ( ! isset( $data_object->labels['archives'] ) && isset( $data_object->labels['all_items'] ) ) {
2219 $data_object->labels['archives'] = $data_object->labels['all_items'];
2220 }
2221
2222 $defaults = array();
2223 foreach ( $nohier_vs_hier_defaults as $key => $value ) {
2224 $defaults[ $key ] = $data_object->hierarchical ? $value[1] : $value[0];
2225 }
2226
2227 $labels = array_merge( $defaults, $data_object->labels );
2228 $data_object->labels = (object) $data_object->labels;
2229
2230 return (object) $labels;
2231}
2232
2233/**
2234 * Adds submenus for post types.
2235 *
2236 * @access private
2237 * @since 3.1.0
2238 */
2239function _add_post_type_submenus() {
2240 foreach ( get_post_types( array( 'show_ui' => true ) ) as $ptype ) {
2241 $ptype_obj = get_post_type_object( $ptype );
2242 // Sub-menus only.
2243 if ( ! $ptype_obj->show_in_menu || true === $ptype_obj->show_in_menu ) {
2244 continue;
2245 }
2246 add_submenu_page( $ptype_obj->show_in_menu, $ptype_obj->labels->name, $ptype_obj->labels->all_items, $ptype_obj->cap->edit_posts, "edit.php?post_type=$ptype" );
2247 }
2248}
2249
2250/**
2251 * Registers support of certain features for a post type.
2252 *
2253 * All core features are directly associated with a functional area of the edit
2254 * screen, such as the editor or a meta box. Features include: 'title', 'editor',
2255 * 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes',
2256 * 'thumbnail', 'custom-fields', and 'post-formats'.
2257 *
2258 * Additionally, the 'revisions' feature dictates whether the post type will
2259 * store revisions, the 'autosave' feature dictates whether the post type
2260 * will be autosaved, and the 'comments' feature dictates whether the comments
2261 * count will show on the edit screen.
2262 *
2263 * A third, optional parameter can also be passed along with a feature to provide
2264 * additional information about supporting that feature.
2265 *
2266 * Example usage:
2267 *
2268 * add_post_type_support( 'my_post_type', 'comments' );
2269 * add_post_type_support( 'my_post_type', array(
2270 * 'author', 'excerpt',
2271 * ) );
2272 * add_post_type_support( 'my_post_type', 'my_feature', array(
2273 * 'field' => 'value',
2274 * ) );
2275 *
2276 * @since 3.0.0
2277 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
2278 * by adding it to the function signature.
2279 *
2280 * @global array $_wp_post_type_features
2281 *
2282 * @param string $post_type The post type for which to add the feature.
2283 * @param string|array $feature The feature being added, accepts an array of
2284 * feature strings or a single string.
2285 * @param mixed ...$args Optional extra arguments to pass along with certain features.
2286 */
2287function add_post_type_support( $post_type, $feature, ...$args ) {
2288 global $_wp_post_type_features;
2289
2290 $features = (array) $feature;
2291 foreach ( $features as $feature ) {
2292 if ( $args ) {
2293 $_wp_post_type_features[ $post_type ][ $feature ] = $args;
2294 } else {
2295 $_wp_post_type_features[ $post_type ][ $feature ] = true;
2296 }
2297 }
2298}
2299
2300/**
2301 * Removes support for a feature from a post type.
2302 *
2303 * @since 3.0.0
2304 *
2305 * @global array $_wp_post_type_features
2306 *
2307 * @param string $post_type The post type for which to remove the feature.
2308 * @param string $feature The feature being removed.
2309 */
2310function remove_post_type_support( $post_type, $feature ) {
2311 global $_wp_post_type_features;
2312
2313 unset( $_wp_post_type_features[ $post_type ][ $feature ] );
2314}
2315
2316/**
2317 * Gets all the post type features
2318 *
2319 * @since 3.4.0
2320 *
2321 * @global array $_wp_post_type_features
2322 *
2323 * @param string $post_type The post type.
2324 * @return array Post type supports list.
2325 */
2326function get_all_post_type_supports( $post_type ) {
2327 global $_wp_post_type_features;
2328
2329 if ( isset( $_wp_post_type_features[ $post_type ] ) ) {
2330 return $_wp_post_type_features[ $post_type ];
2331 }
2332
2333 return array();
2334}
2335
2336/**
2337 * Checks a post type's support for a given feature.
2338 *
2339 * @since 3.0.0
2340 *
2341 * @global array $_wp_post_type_features
2342 *
2343 * @param string $post_type The post type being checked.
2344 * @param string $feature The feature being checked.
2345 * @return bool Whether the post type supports the given feature.
2346 */
2347function post_type_supports( $post_type, $feature ) {
2348 global $_wp_post_type_features;
2349
2350 return ( isset( $_wp_post_type_features[ $post_type ][ $feature ] ) );
2351}
2352/**
2353 * Retrieves a list of post type names that support a specific feature.
2354 *
2355 * @since 4.5.0
2356 *
2357 * @global array $_wp_post_type_features Post type features
2358 *
2359 * @param array|string $feature Single feature or an array of features the post types should support.
2360 * @param string $operator Optional. The logical operation to perform. 'or' means
2361 * only one element from the array needs to match; 'and'
2362 * means all elements must match; 'not' means no elements may
2363 * match. Default 'and'.
2364 * @return string[] A list of post type names.
2365 */
2366function get_post_types_by_support( $feature, $operator = 'and' ) {
2367 global $_wp_post_type_features;
2368
2369 $features = array_fill_keys( (array) $feature, true );
2370
2371 return array_keys( wp_filter_object_list( $_wp_post_type_features, $features, $operator ) );
2372}
2373
2374/**
2375 * Updates the post type for the post ID.
2376 *
2377 * The page or post cache will be cleaned for the post ID.
2378 *
2379 * @since 2.5.0
2380 *
2381 * @global wpdb $wpdb WordPress database abstraction object.
2382 *
2383 * @param int $post_id Optional. Post ID to change post type. Default 0.
2384 * @param string $post_type Optional. Post type. Accepts 'post' or 'page' to
2385 * name a few. Default 'post'.
2386 * @return int|false Amount of rows changed. Should be 1 for success and 0 for failure.
2387 */
2388function set_post_type( $post_id = 0, $post_type = 'post' ) {
2389 global $wpdb;
2390
2391 $post_type = sanitize_post_field( 'post_type', $post_type, $post_id, 'db' );
2392 $return = $wpdb->update( $wpdb->posts, array( 'post_type' => $post_type ), array( 'ID' => $post_id ) );
2393
2394 clean_post_cache( $post_id );
2395
2396 return $return;
2397}
2398
2399/**
2400 * Determines whether a post type is considered "viewable".
2401 *
2402 * For built-in post types such as posts and pages, the 'public' value will be evaluated.
2403 * For all others, the 'publicly_queryable' value will be used.
2404 *
2405 * @since 4.4.0
2406 * @since 4.5.0 Added the ability to pass a post type name in addition to object.
2407 * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object.
2408 * @since 5.9.0 Added `is_post_type_viewable` hook to filter the result.
2409 *
2410 * @param string|WP_Post_Type $post_type Post type name or object.
2411 * @return bool Whether the post type should be considered viewable.
2412 */
2413function is_post_type_viewable( $post_type ) {
2414 if ( is_scalar( $post_type ) ) {
2415 $post_type = get_post_type_object( $post_type );
2416
2417 if ( ! $post_type ) {
2418 return false;
2419 }
2420 }
2421
2422 if ( ! is_object( $post_type ) ) {
2423 return false;
2424 }
2425
2426 $is_viewable = $post_type->publicly_queryable || ( $post_type->_builtin && $post_type->public );
2427
2428 /**
2429 * Filters whether a post type is considered "viewable".
2430 *
2431 * The returned filtered value must be a boolean type to ensure
2432 * `is_post_type_viewable()` only returns a boolean. This strictness
2433 * is by design to maintain backwards-compatibility and guard against
2434 * potential type errors in PHP 8.1+. Non-boolean values (even falsey
2435 * and truthy values) will result in the function returning false.
2436 *
2437 * @since 5.9.0
2438 *
2439 * @param bool $is_viewable Whether the post type is "viewable" (strict type).
2440 * @param WP_Post_Type $post_type Post type object.
2441 */
2442 return true === apply_filters( 'is_post_type_viewable', $is_viewable, $post_type );
2443}
2444
2445/**
2446 * Determines whether a post status is considered "viewable".
2447 *
2448 * For built-in post statuses such as publish and private, the 'public' value will be evaluated.
2449 * For all others, the 'publicly_queryable' value will be used.
2450 *
2451 * @since 5.7.0
2452 * @since 5.9.0 Added `is_post_status_viewable` hook to filter the result.
2453 *
2454 * @param string|stdClass $post_status Post status name or object.
2455 * @return bool Whether the post status should be considered viewable.
2456 */
2457function is_post_status_viewable( $post_status ) {
2458 if ( is_scalar( $post_status ) ) {
2459 if ( ! is_string( $post_status ) ) {
2460 return false;
2461 }
2462
2463 $post_status = get_post_status_object( $post_status );
2464
2465 if ( ! $post_status ) {
2466 return false;
2467 }
2468 }
2469
2470 if (
2471 ! is_object( $post_status ) ||
2472 $post_status->internal ||
2473 $post_status->protected
2474 ) {
2475 return false;
2476 }
2477
2478 $is_viewable = $post_status->publicly_queryable || ( $post_status->_builtin && $post_status->public );
2479
2480 /**
2481 * Filters whether a post status is considered "viewable".
2482 *
2483 * The returned filtered value must be a boolean type to ensure
2484 * `is_post_status_viewable()` only returns a boolean. This strictness
2485 * is by design to maintain backwards-compatibility and guard against
2486 * potential type errors in PHP 8.1+. Non-boolean values (even falsey
2487 * and truthy values) will result in the function returning false.
2488 *
2489 * @since 5.9.0
2490 *
2491 * @param bool $is_viewable Whether the post status is "viewable" (strict type).
2492 * @param stdClass $post_status Post status object.
2493 */
2494 return true === apply_filters( 'is_post_status_viewable', $is_viewable, $post_status );
2495}
2496
2497/**
2498 * Determines whether a post is publicly viewable.
2499 *
2500 * Posts are considered publicly viewable if both the post status and post type
2501 * are viewable.
2502 *
2503 * @since 5.7.0
2504 *
2505 * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
2506 * @return bool Whether the post is publicly viewable.
2507 */
2508function is_post_publicly_viewable( $post = null ) {
2509 $post = get_post( $post );
2510
2511 if ( ! $post ) {
2512 return false;
2513 }
2514
2515 $post_type = get_post_type( $post );
2516 $post_status = get_post_status( $post );
2517
2518 return is_post_type_viewable( $post_type ) && is_post_status_viewable( $post_status );
2519}
2520
2521/**
2522 * Determines whether a post is embeddable.
2523 *
2524 * @since 6.8.0
2525 *
2526 * @param int|WP_Post|null $post Optional. Post ID or `WP_Post` object. Defaults to global $post.
2527 * @return bool Whether the post should be considered embeddable.
2528 */
2529function is_post_embeddable( $post = null ) {
2530 $post = get_post( $post );
2531
2532 if ( ! $post ) {
2533 return false;
2534 }
2535
2536 $post_type = get_post_type_object( $post->post_type );
2537
2538 if ( ! $post_type ) {
2539 return false;
2540 }
2541
2542 $is_embeddable = $post_type->embeddable;
2543
2544 /**
2545 * Filter whether a post is embeddable.
2546 *
2547 * @since 6.8.0
2548 *
2549 * @param bool $is_embeddable Whether the post is embeddable.
2550 * @param WP_Post $post Post object.
2551 */
2552 return apply_filters( 'is_post_embeddable', $is_embeddable, $post );
2553}
2554
2555/**
2556 * Retrieves an array of the latest posts, or posts matching the given criteria.
2557 *
2558 * For more information on the accepted arguments, see the
2559 * {@link https://developer.wordpress.org/reference/classes/wp_query/
2560 * WP_Query} documentation in the Developer Handbook.
2561 *
2562 * The `$ignore_sticky_posts` and `$no_found_rows` arguments are ignored by
2563 * this function and both are set to `true`.
2564 *
2565 * The defaults are as follows:
2566 *
2567 * @since 1.2.0
2568 *
2569 * @see WP_Query
2570 * @see WP_Query::parse_query()
2571 *
2572 * @param array $args {
2573 * Optional. Arguments to retrieve posts. See WP_Query::parse_query() for all available arguments.
2574 *
2575 * @type int $numberposts Total number of posts to retrieve. Is an alias of `$posts_per_page`
2576 * in WP_Query. Accepts -1 for all. Default 5.
2577 * @type int|string $category Category ID or comma-separated list of IDs (this or any children).
2578 * Is an alias of `$cat` in WP_Query. Default 0.
2579 * @type int[] $include An array of post IDs to retrieve, sticky posts will be included.
2580 * Is an alias of `$post__in` in WP_Query. Default empty array.
2581 * @type int[] $exclude An array of post IDs not to retrieve. Default empty array.
2582 * @type bool $suppress_filters Whether to suppress filters. Default true.
2583 * }
2584 * @return WP_Post[]|int[] Array of post objects or post IDs.
2585 */
2586function get_posts( $args = null ) {
2587 $defaults = array(
2588 'numberposts' => 5,
2589 'category' => 0,
2590 'orderby' => 'date',
2591 'order' => 'DESC',
2592 'include' => array(),
2593 'exclude' => array(),
2594 'meta_key' => '',
2595 'meta_value' => '',
2596 'post_type' => 'post',
2597 'suppress_filters' => true,
2598 );
2599
2600 $parsed_args = wp_parse_args( $args, $defaults );
2601 if ( empty( $parsed_args['post_status'] ) ) {
2602 $parsed_args['post_status'] = ( 'attachment' === $parsed_args['post_type'] ) ? 'inherit' : 'publish';
2603 }
2604 if ( ! empty( $parsed_args['numberposts'] ) && empty( $parsed_args['posts_per_page'] ) ) {
2605 $parsed_args['posts_per_page'] = $parsed_args['numberposts'];
2606 }
2607 if ( ! empty( $parsed_args['category'] ) ) {
2608 $parsed_args['cat'] = $parsed_args['category'];
2609 }
2610 if ( ! empty( $parsed_args['include'] ) ) {
2611 $incposts = wp_parse_id_list( $parsed_args['include'] );
2612 $parsed_args['posts_per_page'] = count( $incposts ); // Only the number of posts included.
2613 $parsed_args['post__in'] = $incposts;
2614 } elseif ( ! empty( $parsed_args['exclude'] ) ) {
2615 $parsed_args['post__not_in'] = wp_parse_id_list( $parsed_args['exclude'] );
2616 }
2617
2618 $parsed_args['ignore_sticky_posts'] = true;
2619 $parsed_args['no_found_rows'] = true;
2620
2621 $get_posts = new WP_Query();
2622 return $get_posts->query( $parsed_args );
2623}
2624
2625//
2626// Post meta functions.
2627//
2628
2629/**
2630 * Adds a meta field to the given post.
2631 *
2632 * Post meta data is called "Custom Fields" on the Administration Screen.
2633 *
2634 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
2635 *
2636 * @since 1.5.0
2637 *
2638 * @param int $post_id Post ID.
2639 * @param string $meta_key Metadata name.
2640 * @param mixed $meta_value Metadata value. Arrays and objects are stored as serialized data and
2641 * will be returned as the same type when retrieved. Other data types will
2642 * be stored as strings in the database:
2643 * - false is stored and retrieved as an empty string ('')
2644 * - true is stored and retrieved as '1'
2645 * - numbers (both integer and float) are stored and retrieved as strings
2646 * Must be serializable if non-scalar.
2647 * @param bool $unique Optional. Whether the same key should not be added.
2648 * Default false.
2649 * @return int|false Meta ID on success, false on failure.
2650 */
2651function add_post_meta( $post_id, $meta_key, $meta_value, $unique = false ) {
2652 // Make sure meta is added to the post, not a revision.
2653 $the_post = wp_is_post_revision( $post_id );
2654 if ( $the_post ) {
2655 $post_id = $the_post;
2656 }
2657
2658 return add_metadata( 'post', $post_id, $meta_key, $meta_value, $unique );
2659}
2660
2661/**
2662 * Deletes a post meta field for the given post ID.
2663 *
2664 * You can match based on the key, or key and value. Removing based on key and
2665 * value, will keep from removing duplicate metadata with the same key. It also
2666 * allows removing all metadata matching the key, if needed.
2667 *
2668 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
2669 *
2670 * @since 1.5.0
2671 *
2672 * @param int $post_id Post ID.
2673 * @param string $meta_key Metadata name.
2674 * @param mixed $meta_value Optional. Metadata value. If provided,
2675 * rows will only be removed that match the value.
2676 * Must be serializable if non-scalar. Default empty.
2677 * @return bool True on success, false on failure.
2678 */
2679function delete_post_meta( $post_id, $meta_key, $meta_value = '' ) {
2680 // Make sure meta is deleted from the post, not from a revision.
2681 $the_post = wp_is_post_revision( $post_id );
2682 if ( $the_post ) {
2683 $post_id = $the_post;
2684 }
2685
2686 return delete_metadata( 'post', $post_id, $meta_key, $meta_value );
2687}
2688
2689/**
2690 * Retrieves a post meta field for the given post ID.
2691 *
2692 * @since 1.5.0
2693 *
2694 * @param int $post_id Post ID.
2695 * @param string $key Optional. The meta key to retrieve. By default,
2696 * returns data for all keys. Default empty.
2697 * @param bool $single Optional. Whether to return a single value.
2698 * This parameter has no effect if `$key` is not specified.
2699 * Default false.
2700 * @return mixed An array of values if `$single` is false.
2701 * The value of the meta field if `$single` is true.
2702 * False for an invalid `$post_id` (non-numeric, zero, or negative value).
2703 * An empty array if a valid but non-existing post ID is passed and `$single` is false.
2704 * An empty string if a valid but non-existing post ID is passed and `$single` is true.
2705 * Note: Non-serialized values are returned as strings:
2706 * - false values are returned as empty strings ('')
2707 * - true values are returned as '1'
2708 * - numbers (both integer and float) are returned as strings
2709 * Arrays and objects retain their original type.
2710 */
2711function get_post_meta( $post_id, $key = '', $single = false ) {
2712 return get_metadata( 'post', $post_id, $key, $single );
2713}
2714
2715/**
2716 * Updates a post meta field based on the given post ID.
2717 *
2718 * Use the `$prev_value` parameter to differentiate between meta fields with the
2719 * same key and post ID.
2720 *
2721 * If the meta field for the post does not exist, it will be added and its ID returned.
2722 *
2723 * Can be used in place of add_post_meta().
2724 *
2725 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
2726 *
2727 * @since 1.5.0
2728 *
2729 * @param int $post_id Post ID.
2730 * @param string $meta_key Metadata key.
2731 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
2732 * @param mixed $prev_value Optional. Previous value to check before updating.
2733 * If specified, only update existing metadata entries with
2734 * this value. Otherwise, update all entries. Default empty.
2735 * @return int|bool Meta ID if the key didn't exist, true on successful update,
2736 * false on failure or if the value passed to the function
2737 * is the same as the one that is already in the database.
2738 */
2739function update_post_meta( $post_id, $meta_key, $meta_value, $prev_value = '' ) {
2740 // Make sure meta is updated for the post, not for a revision.
2741 $the_post = wp_is_post_revision( $post_id );
2742 if ( $the_post ) {
2743 $post_id = $the_post;
2744 }
2745
2746 return update_metadata( 'post', $post_id, $meta_key, $meta_value, $prev_value );
2747}
2748
2749/**
2750 * Deletes everything from post meta matching the given meta key.
2751 *
2752 * @since 2.3.0
2753 *
2754 * @param string $post_meta_key Key to search for when deleting.
2755 * @return bool Whether the post meta key was deleted from the database.
2756 */
2757function delete_post_meta_by_key( $post_meta_key ) {
2758 return delete_metadata( 'post', null, $post_meta_key, '', true );
2759}
2760
2761/**
2762 * Registers a meta key for posts.
2763 *
2764 * @since 4.9.8
2765 *
2766 * @param string $post_type Post type to register a meta key for. Pass an empty string
2767 * to register the meta key across all existing post types.
2768 * @param string $meta_key The meta key to register.
2769 * @param array $args Data used to describe the meta key when registered. See
2770 * {@see register_meta()} for a list of supported arguments.
2771 * @return bool True if the meta key was successfully registered, false if not.
2772 */
2773function register_post_meta( $post_type, $meta_key, array $args ) {
2774 $args['object_subtype'] = $post_type;
2775
2776 return register_meta( 'post', $meta_key, $args );
2777}
2778
2779/**
2780 * Unregisters a meta key for posts.
2781 *
2782 * @since 4.9.8
2783 *
2784 * @param string $post_type Post type the meta key is currently registered for. Pass
2785 * an empty string if the meta key is registered across all
2786 * existing post types.
2787 * @param string $meta_key The meta key to unregister.
2788 * @return bool True on success, false if the meta key was not previously registered.
2789 */
2790function unregister_post_meta( $post_type, $meta_key ) {
2791 return unregister_meta_key( 'post', $meta_key, $post_type );
2792}
2793
2794/**
2795 * Retrieves post meta fields, based on post ID.
2796 *
2797 * The post meta fields are retrieved from the cache where possible,
2798 * so the function is optimized to be called more than once.
2799 *
2800 * @since 1.2.0
2801 *
2802 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`.
2803 * @return mixed An array of values.
2804 * False for an invalid `$post_id` (non-numeric, zero, or negative value).
2805 * An empty string if a valid but non-existing post ID is passed.
2806 */
2807function get_post_custom( $post_id = 0 ) {
2808 $post_id = absint( $post_id );
2809
2810 if ( ! $post_id ) {
2811 $post_id = get_the_ID();
2812 }
2813
2814 return get_post_meta( $post_id );
2815}
2816
2817/**
2818 * Retrieves meta field names for a post.
2819 *
2820 * If there are no meta fields, then nothing (null) will be returned.
2821 *
2822 * @since 1.2.0
2823 *
2824 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`.
2825 * @return array|void Array of the keys, if retrieved.
2826 */
2827function get_post_custom_keys( $post_id = 0 ) {
2828 $custom = get_post_custom( $post_id );
2829
2830 if ( ! is_array( $custom ) ) {
2831 return;
2832 }
2833
2834 $keys = array_keys( $custom );
2835 if ( $keys ) {
2836 return $keys;
2837 }
2838}
2839
2840/**
2841 * Retrieves values for a custom post field.
2842 *
2843 * The parameters must not be considered optional. All of the post meta fields
2844 * will be retrieved and only the meta field key values returned.
2845 *
2846 * @since 1.2.0
2847 *
2848 * @param string $key Optional. Meta field key. Default empty.
2849 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`.
2850 * @return array|null Meta field values.
2851 */
2852function get_post_custom_values( $key = '', $post_id = 0 ) {
2853 if ( ! $key ) {
2854 return null;
2855 }
2856
2857 $custom = get_post_custom( $post_id );
2858
2859 return isset( $custom[ $key ] ) ? $custom[ $key ] : null;
2860}
2861
2862/**
2863 * Determines whether a post is sticky.
2864 *
2865 * Sticky posts should remain at the top of The Loop. If the post ID is not
2866 * given, then The Loop ID for the current post will be used.
2867 *
2868 * For more information on this and similar theme functions, check out
2869 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
2870 * Conditional Tags} article in the Theme Developer Handbook.
2871 *
2872 * @since 2.7.0
2873 *
2874 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`.
2875 * @return bool Whether post is sticky.
2876 */
2877function is_sticky( $post_id = 0 ) {
2878 $post_id = absint( $post_id );
2879
2880 if ( ! $post_id ) {
2881 $post_id = get_the_ID();
2882 }
2883
2884 $stickies = get_option( 'sticky_posts' );
2885
2886 if ( is_array( $stickies ) ) {
2887 $stickies = array_map( 'intval', $stickies );
2888 $is_sticky = in_array( $post_id, $stickies, true );
2889 } else {
2890 $is_sticky = false;
2891 }
2892
2893 /**
2894 * Filters whether a post is sticky.
2895 *
2896 * @since 5.3.0
2897 *
2898 * @param bool $is_sticky Whether a post is sticky.
2899 * @param int $post_id Post ID.
2900 */
2901 return apply_filters( 'is_sticky', $is_sticky, $post_id );
2902}
2903
2904/**
2905 * Sanitizes every post field.
2906 *
2907 * If the context is 'raw', then the post object or array will get minimal
2908 * sanitization of the integer fields.
2909 *
2910 * @since 2.3.0
2911 *
2912 * @see sanitize_post_field()
2913 *
2914 * @param object|WP_Post|array $post The post object or array
2915 * @param string $context Optional. How to sanitize post fields.
2916 * Accepts 'raw', 'edit', 'db', 'display',
2917 * 'attribute', or 'js'. Default 'display'.
2918 * @return object|WP_Post|array The now sanitized post object or array (will be the
2919 * same type as `$post`).
2920 */
2921function sanitize_post( $post, $context = 'display' ) {
2922 if ( is_object( $post ) ) {
2923 // Check if post already filtered for this context.
2924 if ( isset( $post->filter ) && $context === $post->filter ) {
2925 return $post;
2926 }
2927 if ( ! isset( $post->ID ) ) {
2928 $post->ID = 0;
2929 }
2930 foreach ( array_keys( get_object_vars( $post ) ) as $field ) {
2931 $post->$field = sanitize_post_field( $field, $post->$field, $post->ID, $context );
2932 }
2933 $post->filter = $context;
2934 } elseif ( is_array( $post ) ) {
2935 // Check if post already filtered for this context.
2936 if ( isset( $post['filter'] ) && $context === $post['filter'] ) {
2937 return $post;
2938 }
2939 if ( ! isset( $post['ID'] ) ) {
2940 $post['ID'] = 0;
2941 }
2942 foreach ( array_keys( $post ) as $field ) {
2943 $post[ $field ] = sanitize_post_field( $field, $post[ $field ], $post['ID'], $context );
2944 }
2945 $post['filter'] = $context;
2946 }
2947 return $post;
2948}
2949
2950/**
2951 * Sanitizes a post field based on context.
2952 *
2953 * Possible context values are: 'raw', 'edit', 'db', 'display', 'attribute' and
2954 * 'js'. The 'display' context is used by default. 'attribute' and 'js' contexts
2955 * are treated like 'display' when calling filters.
2956 *
2957 * @since 2.3.0
2958 * @since 4.4.0 Like `sanitize_post()`, `$context` defaults to 'display'.
2959 *
2960 * @param string $field The Post Object field name.
2961 * @param mixed $value The Post Object value.
2962 * @param int $post_id Post ID.
2963 * @param string $context Optional. How to sanitize the field. Possible values are 'raw', 'edit',
2964 * 'db', 'display', 'attribute' and 'js'. Default 'display'.
2965 * @return mixed Sanitized value.
2966 */
2967function sanitize_post_field( $field, $value, $post_id, $context = 'display' ) {
2968 $int_fields = array( 'ID', 'post_parent', 'menu_order' );
2969 if ( in_array( $field, $int_fields, true ) ) {
2970 $value = (int) $value;
2971 }
2972
2973 // Fields which contain arrays of integers.
2974 $array_int_fields = array( 'ancestors' );
2975 if ( in_array( $field, $array_int_fields, true ) ) {
2976 $value = array_map( 'absint', $value );
2977 return $value;
2978 }
2979
2980 if ( 'raw' === $context ) {
2981 return $value;
2982 }
2983
2984 $prefixed = false;
2985 if ( str_contains( $field, 'post_' ) ) {
2986 $prefixed = true;
2987 $field_no_prefix = str_replace( 'post_', '', $field );
2988 }
2989
2990 if ( 'edit' === $context ) {
2991 $format_to_edit = array( 'post_content', 'post_excerpt', 'post_title', 'post_password' );
2992
2993 if ( $prefixed ) {
2994
2995 /**
2996 * Filters the value of a specific post field to edit.
2997 *
2998 * The dynamic portion of the hook name, `$field`, refers to the post
2999 * field name. Possible filter names include:
3000 *
3001 * - `edit_post_author`
3002 * - `edit_post_date`
3003 * - `edit_post_date_gmt`
3004 * - `edit_post_content`
3005 * - `edit_post_title`
3006 * - `edit_post_excerpt`
3007 * - `edit_post_status`
3008 * - `edit_post_password`
3009 * - `edit_post_name`
3010 * - `edit_post_modified`
3011 * - `edit_post_modified_gmt`
3012 * - `edit_post_content_filtered`
3013 * - `edit_post_parent`
3014 * - `edit_post_type`
3015 * - `edit_post_mime_type`
3016 *
3017 * @since 2.3.0
3018 *
3019 * @param mixed $value Value of the post field.
3020 * @param int $post_id Post ID.
3021 */
3022 $value = apply_filters( "edit_{$field}", $value, $post_id );
3023
3024 /**
3025 * Filters the value of a specific post field to edit.
3026 *
3027 * Only applied to post fields with a name which is prefixed with `post_`.
3028 *
3029 * The dynamic portion of the hook name, `$field_no_prefix`, refers to the
3030 * post field name minus the `post_` prefix. Possible filter names include:
3031 *
3032 * - `author_edit_pre`
3033 * - `date_edit_pre`
3034 * - `date_gmt_edit_pre`
3035 * - `content_edit_pre`
3036 * - `title_edit_pre`
3037 * - `excerpt_edit_pre`
3038 * - `status_edit_pre`
3039 * - `password_edit_pre`
3040 * - `name_edit_pre`
3041 * - `modified_edit_pre`
3042 * - `modified_gmt_edit_pre`
3043 * - `content_filtered_edit_pre`
3044 * - `parent_edit_pre`
3045 * - `type_edit_pre`
3046 * - `mime_type_edit_pre`
3047 *
3048 * @since 2.3.0
3049 *
3050 * @param mixed $value Value of the post field.
3051 * @param int $post_id Post ID.
3052 */
3053 $value = apply_filters( "{$field_no_prefix}_edit_pre", $value, $post_id );
3054 } else {
3055 /**
3056 * Filters the value of a specific post field to edit.
3057 *
3058 * Only applied to post fields not prefixed with `post_`.
3059 *
3060 * The dynamic portion of the hook name, `$field`, refers to the
3061 * post field name. Possible filter names include:
3062 *
3063 * - `edit_post_ID`
3064 * - `edit_post_ping_status`
3065 * - `edit_post_pinged`
3066 * - `edit_post_to_ping`
3067 * - `edit_post_comment_count`
3068 * - `edit_post_comment_status`
3069 * - `edit_post_guid`
3070 * - `edit_post_menu_order`
3071 *
3072 * @since 2.3.0
3073 *
3074 * @param mixed $value Value of the post field.
3075 * @param int $post_id Post ID.
3076 */
3077 $value = apply_filters( "edit_post_{$field}", $value, $post_id );
3078 }
3079
3080 if ( in_array( $field, $format_to_edit, true ) ) {
3081 if ( 'post_content' === $field ) {
3082 $value = format_to_edit( $value, user_can_richedit() );
3083 } else {
3084 $value = format_to_edit( $value );
3085 }
3086 } else {
3087 $value = esc_attr( $value );
3088 }
3089 } elseif ( 'db' === $context ) {
3090 if ( $prefixed ) {
3091
3092 /**
3093 * Filters the value of a specific post field before saving.
3094 *
3095 * Only applied to post fields with a name which is prefixed with `post_`.
3096 *
3097 * The dynamic portion of the hook name, `$field`, refers to the post
3098 * field name. Possible filter names include:
3099 *
3100 * - `pre_post_author`
3101 * - `pre_post_date`
3102 * - `pre_post_date_gmt`
3103 * - `pre_post_content`
3104 * - `pre_post_title`
3105 * - `pre_post_excerpt`
3106 * - `pre_post_status`
3107 * - `pre_post_password`
3108 * - `pre_post_name`
3109 * - `pre_post_modified`
3110 * - `pre_post_modified_gmt`
3111 * - `pre_post_content_filtered`
3112 * - `pre_post_parent`
3113 * - `pre_post_type`
3114 * - `pre_post_mime_type`
3115 *
3116 * @since 2.3.0
3117 *
3118 * @param mixed $value Value of the post field.
3119 */
3120 $value = apply_filters( "pre_{$field}", $value );
3121
3122 /**
3123 * Filters the value of a specific field before saving.
3124 *
3125 * Only applied to post fields with a name which is prefixed with `post_`.
3126 *
3127 * The dynamic portion of the hook name, `$field_no_prefix`, refers to the
3128 * post field name minus the `post_` prefix. Possible filter names include:
3129 *
3130 * - `author_save_pre`
3131 * - `date_save_pre`
3132 * - `date_gmt_save_pre`
3133 * - `content_save_pre`
3134 * - `title_save_pre`
3135 * - `excerpt_save_pre`
3136 * - `status_save_pre`
3137 * - `password_save_pre`
3138 * - `name_save_pre`
3139 * - `modified_save_pre`
3140 * - `modified_gmt_save_pre`
3141 * - `content_filtered_save_pre`
3142 * - `parent_save_pre`
3143 * - `type_save_pre`
3144 * - `mime_type_save_pre`
3145 *
3146 * @since 2.3.0
3147 *
3148 * @param mixed $value Value of the post field.
3149 */
3150 $value = apply_filters( "{$field_no_prefix}_save_pre", $value );
3151 } else {
3152 /**
3153 * Filters the value of a specific field before saving.
3154 *
3155 * Only applied to post fields with a name which is prefixed with `post_`.
3156 *
3157 * The dynamic portion of the hook name, `$field_no_prefix`, refers to the
3158 * post field name minus the `post_` prefix. Possible filter names include:
3159 *
3160 * - `pre_post_ID`
3161 * - `pre_post_comment_status`
3162 * - `pre_post_ping_status`
3163 * - `pre_post_to_ping`
3164 * - `pre_post_pinged`
3165 * - `pre_post_guid`
3166 * - `pre_post_menu_order`
3167 * - `pre_post_comment_count`
3168 *
3169 * @since 2.3.0
3170 *
3171 * @param mixed $value Value of the post field.
3172 */
3173 $value = apply_filters( "pre_post_{$field}", $value );
3174
3175 /**
3176 * Filters the value of a specific post field before saving.
3177 *
3178 * Only applied to post fields with a name which is *not* prefixed with `post_`.
3179 *
3180 * The dynamic portion of the hook name, `$field`, refers to the post
3181 * field name. Possible filter names include:
3182 *
3183 * - `ID_pre`
3184 * - `comment_status_pre`
3185 * - `ping_status_pre`
3186 * - `to_ping_pre`
3187 * - `pinged_pre`
3188 * - `guid_pre`
3189 * - `menu_order_pre`
3190 * - `comment_count_pre`
3191 *
3192 * @since 2.3.0
3193 *
3194 * @param mixed $value Value of the post field.
3195 */
3196 $value = apply_filters( "{$field}_pre", $value );
3197 }
3198 } else {
3199
3200 // Use display filters by default.
3201 if ( $prefixed ) {
3202
3203 /**
3204 * Filters the value of a specific post field for display.
3205 *
3206 * Only applied to post fields with a name which is prefixed with `post_`.
3207 *
3208 * The dynamic portion of the hook name, `$field`, refers to the post
3209 * field name. Possible filter names include:
3210 *
3211 * - `post_author`
3212 * - `post_date`
3213 * - `post_date_gmt`
3214 * - `post_content`
3215 * - `post_title`
3216 * - `post_excerpt`
3217 * - `post_status`
3218 * - `post_password`
3219 * - `post_name`
3220 * - `post_modified`
3221 * - `post_modified_gmt`
3222 * - `post_content_filtered`
3223 * - `post_parent`
3224 * - `post_type`
3225 * - `post_mime_type`
3226 *
3227 * @since 2.3.0
3228 *
3229 * @param mixed $value Value of the prefixed post field.
3230 * @param int $post_id Post ID.
3231 * @param string $context Context for how to sanitize the field.
3232 * Accepts 'raw', 'edit', 'db', 'display',
3233 * 'attribute', or 'js'. Default 'display'.
3234 */
3235 $value = apply_filters( "{$field}", $value, $post_id, $context );
3236 } else {
3237 /**
3238 * Filters the value of a specific post field for display.
3239 *
3240 * Only applied to post fields name which is *not* prefixed with `post_`.
3241 *
3242 * The dynamic portion of the hook name, `$field`, refers to the post
3243 * field name. Possible filter names include:
3244 *
3245 * - `post_ID`
3246 * - `post_comment_status`
3247 * - `post_ping_status`
3248 * - `post_to_ping`
3249 * - `post_pinged`
3250 * - `post_guid`
3251 * - `post_menu_order`
3252 * - `post_comment_count`
3253 *
3254 * @since 2.3.0
3255 *
3256 * @param mixed $value Value of the unprefixed post field.
3257 * @param int $post_id Post ID
3258 * @param string $context Context for how to sanitize the field.
3259 * Accepts 'raw', 'edit', 'db', 'display',
3260 * 'attribute', or 'js'. Default 'display'.
3261 */
3262 $value = apply_filters( "post_{$field}", $value, $post_id, $context );
3263 }
3264
3265 if ( 'attribute' === $context ) {
3266 $value = esc_attr( $value );
3267 } elseif ( 'js' === $context ) {
3268 $value = esc_js( $value );
3269 }
3270 }
3271
3272 // Restore the type for integer fields after esc_attr().
3273 if ( in_array( $field, $int_fields, true ) ) {
3274 $value = (int) $value;
3275 }
3276 return $value;
3277}
3278
3279/**
3280 * Makes a post sticky.
3281 *
3282 * Sticky posts should be displayed at the top of the front page.
3283 *
3284 * @since 2.7.0
3285 *
3286 * @param int $post_id Post ID.
3287 */
3288function stick_post( $post_id ) {
3289 $post_id = (int) $post_id;
3290 $stickies = get_option( 'sticky_posts' );
3291 $updated = false;
3292
3293 if ( ! is_array( $stickies ) ) {
3294 $stickies = array();
3295 } else {
3296 $stickies = array_unique( array_map( 'intval', $stickies ) );
3297 }
3298
3299 if ( ! in_array( $post_id, $stickies, true ) ) {
3300 $stickies[] = $post_id;
3301 $updated = update_option( 'sticky_posts', array_values( $stickies ) );
3302 }
3303
3304 if ( $updated ) {
3305 /**
3306 * Fires once a post has been added to the sticky list.
3307 *
3308 * @since 4.6.0
3309 *
3310 * @param int $post_id ID of the post that was stuck.
3311 */
3312 do_action( 'post_stuck', $post_id );
3313 }
3314}
3315
3316/**
3317 * Un-sticks a post.
3318 *
3319 * Sticky posts should be displayed at the top of the front page.
3320 *
3321 * @since 2.7.0
3322 *
3323 * @param int $post_id Post ID.
3324 */
3325function unstick_post( $post_id ) {
3326 $post_id = (int) $post_id;
3327 $stickies = get_option( 'sticky_posts' );
3328
3329 if ( ! is_array( $stickies ) ) {
3330 return;
3331 }
3332
3333 $stickies = array_values( array_unique( array_map( 'intval', $stickies ) ) );
3334
3335 if ( ! in_array( $post_id, $stickies, true ) ) {
3336 return;
3337 }
3338
3339 $offset = array_search( $post_id, $stickies, true );
3340 if ( false === $offset ) {
3341 return;
3342 }
3343
3344 array_splice( $stickies, $offset, 1 );
3345
3346 $updated = update_option( 'sticky_posts', $stickies );
3347
3348 if ( $updated ) {
3349 /**
3350 * Fires once a post has been removed from the sticky list.
3351 *
3352 * @since 4.6.0
3353 *
3354 * @param int $post_id ID of the post that was unstuck.
3355 */
3356 do_action( 'post_unstuck', $post_id );
3357 }
3358}
3359
3360/**
3361 * Returns the cache key for wp_count_posts() based on the passed arguments.
3362 *
3363 * @since 3.9.0
3364 * @access private
3365 *
3366 * @param string $type Optional. Post type to retrieve count Default 'post'.
3367 * @param string $perm Optional. 'readable' or empty. Default empty.
3368 * @return string The cache key.
3369 */
3370function _count_posts_cache_key( $type = 'post', $perm = '' ) {
3371 $cache_key = 'posts-' . $type;
3372
3373 if ( 'readable' === $perm && is_user_logged_in() ) {
3374 $post_type_object = get_post_type_object( $type );
3375
3376 if ( $post_type_object && ! current_user_can( $post_type_object->cap->read_private_posts ) ) {
3377 $cache_key .= '_' . $perm . '_' . get_current_user_id();
3378 }
3379 }
3380
3381 return $cache_key;
3382}
3383
3384/**
3385 * Counts number of posts of a post type and if user has permissions to view.
3386 *
3387 * This function provides an efficient method of finding the amount of post's
3388 * type a blog has. Another method is to count the amount of items in
3389 * get_posts(), but that method has a lot of overhead with doing so. Therefore,
3390 * when developing for 2.5+, use this function instead.
3391 *
3392 * The $perm parameter checks for 'readable' value and if the user can read
3393 * private posts, it will display that for the user that is signed in.
3394 *
3395 * @since 2.5.0
3396 *
3397 * @global wpdb $wpdb WordPress database abstraction object.
3398 *
3399 * @param string $type Optional. Post type to retrieve count. Default 'post'.
3400 * @param string $perm Optional. 'readable' or empty. Default empty.
3401 * @return stdClass An object containing the number of posts for each status,
3402 * or an empty object if the post type does not exist.
3403 */
3404function wp_count_posts( $type = 'post', $perm = '' ) {
3405 global $wpdb;
3406
3407 if ( ! post_type_exists( $type ) ) {
3408 return new stdClass();
3409 }
3410
3411 $cache_key = _count_posts_cache_key( $type, $perm );
3412
3413 $counts = wp_cache_get( $cache_key, 'counts' );
3414 if ( false !== $counts ) {
3415 // We may have cached this before every status was registered.
3416 foreach ( get_post_stati() as $status ) {
3417 if ( ! isset( $counts->{$status} ) ) {
3418 $counts->{$status} = 0;
3419 }
3420 }
3421
3422 /** This filter is documented in wp-includes/post.php */
3423 return apply_filters( 'wp_count_posts', $counts, $type, $perm );
3424 }
3425
3426 if (
3427 'readable' === $perm &&
3428 is_user_logged_in() &&
3429 ! current_user_can( get_post_type_object( $type )->cap->read_private_posts )
3430 ) {
3431 // Optimized query uses subqueries which can leverage DB indexes for better performance. See #61097.
3432 $query = "
3433 SELECT post_status, COUNT(*) AS num_posts
3434 FROM (
3435 SELECT post_status
3436 FROM {$wpdb->posts}
3437 WHERE post_type = %s AND post_status != 'private'
3438 UNION ALL
3439 SELECT post_status
3440 FROM {$wpdb->posts}
3441 WHERE post_type = %s AND post_status = 'private' AND post_author = %d
3442 ) AS filtered_posts
3443 ";
3444 $args = array( $type, $type, get_current_user_id() );
3445 } else {
3446 $query = "
3447 SELECT post_status, COUNT(*) AS num_posts
3448 FROM {$wpdb->posts}
3449 WHERE post_type = %s
3450 ";
3451 $args = array( $type );
3452 }
3453
3454 $query .= ' GROUP BY post_status';
3455
3456 $results = (array) $wpdb->get_results(
3457 $wpdb->prepare( $query, ...$args ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Placeholders are used in the string contained in the variable.
3458 ARRAY_A
3459 );
3460 $counts = array_fill_keys( get_post_stati(), 0 );
3461
3462 foreach ( $results as $row ) {
3463 $counts[ $row['post_status'] ] = $row['num_posts'];
3464 }
3465
3466 $counts = (object) $counts;
3467 wp_cache_set( $cache_key, $counts, 'counts' );
3468
3469 /**
3470 * Filters the post counts by status for the current post type.
3471 *
3472 * @since 3.7.0
3473 *
3474 * @param stdClass $counts An object containing the current post_type's post
3475 * counts by status.
3476 * @param string $type Post type.
3477 * @param string $perm The permission to determine if the posts are 'readable'
3478 * by the current user.
3479 */
3480 return apply_filters( 'wp_count_posts', $counts, $type, $perm );
3481}
3482
3483/**
3484 * Counts number of attachments for the mime type(s).
3485 *
3486 * If you set the optional mime_type parameter, then an array will still be
3487 * returned, but will only have the item you are looking for. It does not give
3488 * you the number of attachments that are children of a post. You can get that
3489 * by counting the number of children that post has.
3490 *
3491 * @since 2.5.0
3492 *
3493 * @global wpdb $wpdb WordPress database abstraction object.
3494 *
3495 * @param string|string[] $mime_type Optional. Array or comma-separated list of
3496 * MIME patterns. Default empty.
3497 * @return stdClass An object containing the attachment counts by mime type.
3498 */
3499function wp_count_attachments( $mime_type = '' ) {
3500 global $wpdb;
3501
3502 $cache_key = sprintf(
3503 'attachments%s',
3504 ! empty( $mime_type ) ? ':' . str_replace( '/', '_', implode( '-', (array) $mime_type ) ) : ''
3505 );
3506
3507 $counts = wp_cache_get( $cache_key, 'counts' );
3508
3509 if ( false === $counts ) {
3510 $and = wp_post_mime_type_where( $mime_type );
3511 $count = $wpdb->get_results( "SELECT post_mime_type, COUNT( * ) AS num_posts FROM $wpdb->posts WHERE post_type = 'attachment' AND post_status != 'trash' $and GROUP BY post_mime_type", ARRAY_A );
3512
3513 $counts = array();
3514 foreach ( (array) $count as $row ) {
3515 $counts[ $row['post_mime_type'] ] = $row['num_posts'];
3516 }
3517 $counts['trash'] = $wpdb->get_var( "SELECT COUNT( * ) FROM $wpdb->posts WHERE post_type = 'attachment' AND post_status = 'trash' $and" );
3518
3519 wp_cache_set( $cache_key, (object) $counts, 'counts' );
3520 }
3521
3522 /**
3523 * Filters the attachment counts by mime type.
3524 *
3525 * @since 3.7.0
3526 *
3527 * @param stdClass $counts An object containing the attachment counts by
3528 * mime type.
3529 * @param string|string[] $mime_type Array or comma-separated list of MIME patterns.
3530 */
3531 return apply_filters( 'wp_count_attachments', (object) $counts, $mime_type );
3532}
3533
3534/**
3535 * Gets default post mime types.
3536 *
3537 * @since 2.9.0
3538 * @since 5.3.0 Added the 'Documents', 'Spreadsheets', and 'Archives' mime type groups.
3539 *
3540 * @return array List of post mime types.
3541 */
3542function get_post_mime_types() {
3543 $post_mime_types = array( // array( adj, noun )
3544 'image' => array(
3545 __( 'Images' ),
3546 __( 'Manage Images' ),
3547 /* translators: %s: Number of images. */
3548 _n_noop(
3549 'Image <span class="count">(%s)</span>',
3550 'Images <span class="count">(%s)</span>'
3551 ),
3552 ),
3553 'audio' => array(
3554 _x( 'Audio', 'file type group' ),
3555 __( 'Manage Audio' ),
3556 /* translators: %s: Number of audio files. */
3557 _n_noop(
3558 'Audio <span class="count">(%s)</span>',
3559 'Audio <span class="count">(%s)</span>'
3560 ),
3561 ),
3562 'video' => array(
3563 _x( 'Video', 'file type group' ),
3564 __( 'Manage Video' ),
3565 /* translators: %s: Number of video files. */
3566 _n_noop(
3567 'Video <span class="count">(%s)</span>',
3568 'Video <span class="count">(%s)</span>'
3569 ),
3570 ),
3571 'document' => array(
3572 __( 'Documents' ),
3573 __( 'Manage Documents' ),
3574 /* translators: %s: Number of documents. */
3575 _n_noop(
3576 'Document <span class="count">(%s)</span>',
3577 'Documents <span class="count">(%s)</span>'
3578 ),
3579 ),
3580 'spreadsheet' => array(
3581 __( 'Spreadsheets' ),
3582 __( 'Manage Spreadsheets' ),
3583 /* translators: %s: Number of spreadsheets. */
3584 _n_noop(
3585 'Spreadsheet <span class="count">(%s)</span>',
3586 'Spreadsheets <span class="count">(%s)</span>'
3587 ),
3588 ),
3589 'archive' => array(
3590 _x( 'Archives', 'file type group' ),
3591 __( 'Manage Archives' ),
3592 /* translators: %s: Number of archives. */
3593 _n_noop(
3594 'Archive <span class="count">(%s)</span>',
3595 'Archives <span class="count">(%s)</span>'
3596 ),
3597 ),
3598 );
3599
3600 $ext_types = wp_get_ext_types();
3601 $mime_types = wp_get_mime_types();
3602
3603 foreach ( $post_mime_types as $group => $labels ) {
3604 if ( in_array( $group, array( 'image', 'audio', 'video' ), true ) ) {
3605 continue;
3606 }
3607
3608 if ( ! isset( $ext_types[ $group ] ) ) {
3609 unset( $post_mime_types[ $group ] );
3610 continue;
3611 }
3612
3613 $group_mime_types = array();
3614 foreach ( $ext_types[ $group ] as $extension ) {
3615 foreach ( $mime_types as $exts => $mime ) {
3616 if ( preg_match( '!^(' . $exts . ')$!i', $extension ) ) {
3617 $group_mime_types[] = $mime;
3618 break;
3619 }
3620 }
3621 }
3622 $group_mime_types = implode( ',', array_unique( $group_mime_types ) );
3623
3624 $post_mime_types[ $group_mime_types ] = $labels;
3625 unset( $post_mime_types[ $group ] );
3626 }
3627
3628 /**
3629 * Filters the default list of post mime types.
3630 *
3631 * @since 2.5.0
3632 *
3633 * @param array $post_mime_types Default list of post mime types.
3634 */
3635 return apply_filters( 'post_mime_types', $post_mime_types );
3636}
3637
3638/**
3639 * Checks a MIME-Type against a list.
3640 *
3641 * If the `$wildcard_mime_types` parameter is a string, it must be comma separated
3642 * list. If the `$real_mime_types` is a string, it is also comma separated to
3643 * create the list.
3644 *
3645 * @since 2.5.0
3646 *
3647 * @param string|string[] $wildcard_mime_types Mime types, e.g. `audio/mpeg`, `image` (same as `image/*`),
3648 * or `flash` (same as `*flash*`).
3649 * @param string|string[] $real_mime_types Real post mime type values.
3650 * @return array array(wildcard=>array(real types)).
3651 */
3652function wp_match_mime_types( $wildcard_mime_types, $real_mime_types ) {
3653 $matches = array();
3654 if ( is_string( $wildcard_mime_types ) ) {
3655 $wildcard_mime_types = array_map( 'trim', explode( ',', $wildcard_mime_types ) );
3656 }
3657 if ( is_string( $real_mime_types ) ) {
3658 $real_mime_types = array_map( 'trim', explode( ',', $real_mime_types ) );
3659 }
3660
3661 $patternses = array();
3662 $wild = '[-._a-z0-9]*';
3663
3664 foreach ( (array) $wildcard_mime_types as $type ) {
3665 $mimes = array_map( 'trim', explode( ',', $type ) );
3666 foreach ( $mimes as $mime ) {
3667 $regex = str_replace( '__wildcard__', $wild, preg_quote( str_replace( '*', '__wildcard__', $mime ) ) );
3668
3669 $patternses[][ $type ] = "^$regex$";
3670
3671 if ( ! str_contains( $mime, '/' ) ) {
3672 $patternses[][ $type ] = "^$regex/";
3673 $patternses[][ $type ] = $regex;
3674 }
3675 }
3676 }
3677 asort( $patternses );
3678
3679 foreach ( $patternses as $patterns ) {
3680 foreach ( $patterns as $type => $pattern ) {
3681 foreach ( (array) $real_mime_types as $real ) {
3682 if ( preg_match( "#$pattern#", $real )
3683 && ( empty( $matches[ $type ] ) || false === array_search( $real, $matches[ $type ], true ) )
3684 ) {
3685 $matches[ $type ][] = $real;
3686 }
3687 }
3688 }
3689 }
3690
3691 return $matches;
3692}
3693
3694/**
3695 * Converts MIME types into SQL.
3696 *
3697 * @since 2.5.0
3698 *
3699 * @param string|string[] $post_mime_types List of mime types or comma separated string
3700 * of mime types.
3701 * @param string $table_alias Optional. Specify a table alias, if needed.
3702 * Default empty.
3703 * @return string The SQL AND clause for mime searching.
3704 */
3705function wp_post_mime_type_where( $post_mime_types, $table_alias = '' ) {
3706 $where = '';
3707 $wildcards = array( '', '%', '%/%' );
3708 if ( is_string( $post_mime_types ) ) {
3709 $post_mime_types = array_map( 'trim', explode( ',', $post_mime_types ) );
3710 }
3711
3712 $where_clauses = array();
3713
3714 foreach ( (array) $post_mime_types as $mime_type ) {
3715 $mime_type = preg_replace( '/\s/', '', $mime_type );
3716 $slashpos = strpos( $mime_type, '/' );
3717 if ( false !== $slashpos ) {
3718 $mime_group = preg_replace( '/[^-*.a-zA-Z0-9]/', '', substr( $mime_type, 0, $slashpos ) );
3719 $mime_subgroup = preg_replace( '/[^-*.+a-zA-Z0-9]/', '', substr( $mime_type, $slashpos + 1 ) );
3720 if ( empty( $mime_subgroup ) ) {
3721 $mime_subgroup = '*';
3722 } else {
3723 $mime_subgroup = str_replace( '/', '', $mime_subgroup );
3724 }
3725 $mime_pattern = "$mime_group/$mime_subgroup";
3726 } else {
3727 $mime_pattern = preg_replace( '/[^-*.a-zA-Z0-9]/', '', $mime_type );
3728 if ( ! str_contains( $mime_pattern, '*' ) ) {
3729 $mime_pattern .= '/*';
3730 }
3731 }
3732
3733 $mime_pattern = preg_replace( '/\*+/', '%', $mime_pattern );
3734
3735 if ( in_array( $mime_type, $wildcards, true ) ) {
3736 return '';
3737 }
3738
3739 if ( str_contains( $mime_pattern, '%' ) ) {
3740 $where_clauses[] = empty( $table_alias ) ? "post_mime_type LIKE '$mime_pattern'" : "$table_alias.post_mime_type LIKE '$mime_pattern'";
3741 } else {
3742 $where_clauses[] = empty( $table_alias ) ? "post_mime_type = '$mime_pattern'" : "$table_alias.post_mime_type = '$mime_pattern'";
3743 }
3744 }
3745
3746 if ( ! empty( $where_clauses ) ) {
3747 $where = ' AND (' . implode( ' OR ', $where_clauses ) . ') ';
3748 }
3749
3750 return $where;
3751}
3752
3753/**
3754 * Trashes or deletes a post or page.
3755 *
3756 * When the post and page is permanently deleted, everything that is tied to
3757 * it is deleted also. This includes comments, post meta fields, and terms
3758 * associated with the post.
3759 *
3760 * The post or page is moved to Trash instead of permanently deleted unless
3761 * Trash is disabled, item is already in the Trash, or $force_delete is true.
3762 *
3763 * @since 1.0.0
3764 *
3765 * @global wpdb $wpdb WordPress database abstraction object.
3766 * @see wp_delete_attachment()
3767 * @see wp_trash_post()
3768 *
3769 * @param int $post_id Post ID. (The default of 0 is for historical reasons; providing it is incorrect.)
3770 * @param bool $force_delete Optional. Whether to bypass Trash and force deletion.
3771 * Default false.
3772 * @return WP_Post|false|null Post data on success, false or null on failure.
3773 */
3774function wp_delete_post( $post_id = 0, $force_delete = false ) {
3775 global $wpdb;
3776
3777 $post_id = (int) $post_id;
3778 if ( $post_id <= 0 ) {
3779 _doing_it_wrong( __FUNCTION__, __( 'The post ID must be greater than 0.' ), '6.9.0' );
3780 return false;
3781 }
3782
3783 $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) );
3784
3785 if ( ! $post ) {
3786 return $post;
3787 }
3788
3789 $post = get_post( $post );
3790
3791 if ( ! $force_delete
3792 && ( 'post' === $post->post_type || 'page' === $post->post_type )
3793 && 'trash' !== get_post_status( $post_id ) && EMPTY_TRASH_DAYS
3794 ) {
3795 return wp_trash_post( $post_id );
3796 }
3797
3798 if ( 'attachment' === $post->post_type ) {
3799 return wp_delete_attachment( $post_id, $force_delete );
3800 }
3801
3802 /**
3803 * Filters whether a post deletion should take place.
3804 *
3805 * @since 4.4.0
3806 *
3807 * @param WP_Post|false|null $check Whether to go forward with deletion. Anything other than null will short-circuit deletion.
3808 * @param WP_Post $post Post object.
3809 * @param bool $force_delete Whether to bypass the Trash.
3810 */
3811 $check = apply_filters( 'pre_delete_post', null, $post, $force_delete );
3812 if ( null !== $check ) {
3813 return $check;
3814 }
3815
3816 /**
3817 * Fires before a post is deleted, at the start of wp_delete_post().
3818 *
3819 * @since 3.2.0
3820 * @since 5.5.0 Added the `$post` parameter.
3821 *
3822 * @see wp_delete_post()
3823 *
3824 * @param int $post_id Post ID.
3825 * @param WP_Post $post Post object.
3826 */
3827 do_action( 'before_delete_post', $post_id, $post );
3828
3829 delete_post_meta( $post_id, '_wp_trash_meta_status' );
3830 delete_post_meta( $post_id, '_wp_trash_meta_time' );
3831
3832 wp_delete_object_term_relationships( $post_id, get_object_taxonomies( $post->post_type ) );
3833
3834 $parent_data = array( 'post_parent' => $post->post_parent );
3835 $parent_where = array( 'post_parent' => $post_id );
3836
3837 if ( is_post_type_hierarchical( $post->post_type ) ) {
3838 // Point children of this page to its parent, also clean the cache of affected children.
3839 $children_query = $wpdb->prepare(
3840 "SELECT * FROM $wpdb->posts WHERE post_parent = %d AND post_type = %s",
3841 $post_id,
3842 $post->post_type
3843 );
3844
3845 $children = $wpdb->get_results( $children_query );
3846
3847 if ( $children ) {
3848 $wpdb->update( $wpdb->posts, $parent_data, $parent_where + array( 'post_type' => $post->post_type ) );
3849 }
3850 }
3851
3852 // Do raw query. wp_get_post_revisions() is filtered.
3853 $revision_ids = $wpdb->get_col(
3854 $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'revision'", $post_id )
3855 );
3856
3857 // Use wp_delete_post (via wp_delete_post_revision) again. Ensures any meta/misplaced data gets cleaned up.
3858 foreach ( $revision_ids as $revision_id ) {
3859 wp_delete_post_revision( $revision_id );
3860 }
3861
3862 // Point all attachments to this post up one level.
3863 $wpdb->update( $wpdb->posts, $parent_data, $parent_where + array( 'post_type' => 'attachment' ) );
3864
3865 wp_defer_comment_counting( true );
3866
3867 $comment_ids = $wpdb->get_col(
3868 $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $post_id )
3869 );
3870
3871 foreach ( $comment_ids as $comment_id ) {
3872 wp_delete_comment( $comment_id, true );
3873 }
3874
3875 wp_defer_comment_counting( false );
3876
3877 $post_meta_ids = $wpdb->get_col(
3878 $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $post_id )
3879 );
3880
3881 foreach ( $post_meta_ids as $mid ) {
3882 delete_metadata_by_mid( 'post', $mid );
3883 }
3884
3885 /**
3886 * Fires immediately before a post is deleted from the database.
3887 *
3888 * The dynamic portion of the hook name, `$post->post_type`, refers to
3889 * the post type slug.
3890 *
3891 * @since 6.6.0
3892 *
3893 * @param int $post_id Post ID.
3894 * @param WP_Post $post Post object.
3895 */
3896 do_action( "delete_post_{$post->post_type}", $post_id, $post );
3897
3898 /**
3899 * Fires immediately before a post is deleted from the database.
3900 *
3901 * @since 1.2.0
3902 * @since 5.5.0 Added the `$post` parameter.
3903 *
3904 * @param int $post_id Post ID.
3905 * @param WP_Post $post Post object.
3906 */
3907 do_action( 'delete_post', $post_id, $post );
3908
3909 $result = $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) );
3910 if ( ! $result ) {
3911 return false;
3912 }
3913
3914 /**
3915 * Fires immediately after a post is deleted from the database.
3916 *
3917 * The dynamic portion of the hook name, `$post->post_type`, refers to
3918 * the post type slug.
3919 *
3920 * @since 6.6.0
3921 *
3922 * @param int $post_id Post ID.
3923 * @param WP_Post $post Post object.
3924 */
3925 do_action( "deleted_post_{$post->post_type}", $post_id, $post );
3926
3927 /**
3928 * Fires immediately after a post is deleted from the database.
3929 *
3930 * @since 2.2.0
3931 * @since 5.5.0 Added the `$post` parameter.
3932 *
3933 * @param int $post_id Post ID.
3934 * @param WP_Post $post Post object.
3935 */
3936 do_action( 'deleted_post', $post_id, $post );
3937
3938 clean_post_cache( $post );
3939
3940 if ( is_post_type_hierarchical( $post->post_type ) && $children ) {
3941 foreach ( $children as $child ) {
3942 clean_post_cache( $child );
3943 }
3944 }
3945
3946 wp_clear_scheduled_hook( 'publish_future_post', array( $post_id ) );
3947
3948 /**
3949 * Fires after a post is deleted, at the conclusion of wp_delete_post().
3950 *
3951 * @since 3.2.0
3952 * @since 5.5.0 Added the `$post` parameter.
3953 *
3954 * @see wp_delete_post()
3955 *
3956 * @param int $post_id Post ID.
3957 * @param WP_Post $post Post object.
3958 */
3959 do_action( 'after_delete_post', $post_id, $post );
3960
3961 return $post;
3962}
3963
3964/**
3965 * Resets the page_on_front, show_on_front, and page_for_post settings when
3966 * a linked page is deleted or trashed.
3967 *
3968 * Also ensures the post is no longer sticky.
3969 *
3970 * @since 3.7.0
3971 * @access private
3972 *
3973 * @param int $post_id Post ID.
3974 */
3975function _reset_front_page_settings_for_post( $post_id ) {
3976 $post = get_post( $post_id );
3977
3978 if ( 'page' === $post->post_type ) {
3979 /*
3980 * If the page is defined in option page_on_front or post_for_posts,
3981 * adjust the corresponding options.
3982 */
3983 if ( (int) get_option( 'page_on_front' ) === $post->ID ) {
3984 update_option( 'show_on_front', 'posts' );
3985 update_option( 'page_on_front', 0 );
3986 }
3987 if ( (int) get_option( 'page_for_posts' ) === $post->ID ) {
3988 update_option( 'page_for_posts', 0 );
3989 }
3990 }
3991
3992 unstick_post( $post->ID );
3993}
3994
3995/**
3996 * Moves a post or page to the Trash
3997 *
3998 * If Trash is disabled, the post or page is permanently deleted.
3999 *
4000 * @since 2.9.0
4001 *
4002 * @see wp_delete_post()
4003 *
4004 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`
4005 * if `EMPTY_TRASH_DAYS` equals true.
4006 * @return WP_Post|false|null Post data on success, false or null on failure.
4007 */
4008function wp_trash_post( $post_id = 0 ) {
4009 if ( ! EMPTY_TRASH_DAYS ) {
4010 return wp_delete_post( $post_id, true );
4011 }
4012
4013 $post = get_post( $post_id );
4014
4015 if ( ! $post ) {
4016 return $post;
4017 }
4018
4019 if ( 'trash' === $post->post_status ) {
4020 return false;
4021 }
4022
4023 $previous_status = $post->post_status;
4024
4025 /**
4026 * Filters whether a post trashing should take place.
4027 *
4028 * @since 4.9.0
4029 * @since 6.3.0 Added the `$previous_status` parameter.
4030 *
4031 * @param bool|null $trash Whether to go forward with trashing.
4032 * @param WP_Post $post Post object.
4033 * @param string $previous_status The status of the post about to be trashed.
4034 */
4035 $check = apply_filters( 'pre_trash_post', null, $post, $previous_status );
4036
4037 if ( null !== $check ) {
4038 return $check;
4039 }
4040
4041 /**
4042 * Fires before a post is sent to the Trash.
4043 *
4044 * @since 3.3.0
4045 * @since 6.3.0 Added the `$previous_status` parameter.
4046 *
4047 * @param int $post_id Post ID.
4048 * @param string $previous_status The status of the post about to be trashed.
4049 */
4050 do_action( 'wp_trash_post', $post_id, $previous_status );
4051
4052 add_post_meta( $post_id, '_wp_trash_meta_status', $previous_status );
4053 add_post_meta( $post_id, '_wp_trash_meta_time', time() );
4054
4055 $post_updated = wp_update_post(
4056 array(
4057 'ID' => $post_id,
4058 'post_status' => 'trash',
4059 )
4060 );
4061
4062 if ( ! $post_updated ) {
4063 return false;
4064 }
4065
4066 wp_trash_post_comments( $post_id );
4067
4068 /**
4069 * Fires after a post is sent to the Trash.
4070 *
4071 * @since 2.9.0
4072 * @since 6.3.0 Added the `$previous_status` parameter.
4073 *
4074 * @param int $post_id Post ID.
4075 * @param string $previous_status The status of the post at the point where it was trashed.
4076 */
4077 do_action( 'trashed_post', $post_id, $previous_status );
4078
4079 return $post;
4080}
4081
4082/**
4083 * Restores a post from the Trash.
4084 *
4085 * @since 2.9.0
4086 * @since 5.6.0 An untrashed post is now returned to 'draft' status by default, except for
4087 * attachments which are returned to their original 'inherit' status.
4088 *
4089 * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`.
4090 * @return WP_Post|false|null Post data on success, false or null on failure.
4091 */
4092function wp_untrash_post( $post_id = 0 ) {
4093 $post = get_post( $post_id );
4094
4095 if ( ! $post ) {
4096 return $post;
4097 }
4098
4099 $post_id = $post->ID;
4100
4101 if ( 'trash' !== $post->post_status ) {
4102 return false;
4103 }
4104
4105 $previous_status = get_post_meta( $post_id, '_wp_trash_meta_status', true );
4106
4107 /**
4108 * Filters whether a post untrashing should take place.
4109 *
4110 * @since 4.9.0
4111 * @since 5.6.0 Added the `$previous_status` parameter.
4112 *
4113 * @param bool|null $untrash Whether to go forward with untrashing.
4114 * @param WP_Post $post Post object.
4115 * @param string $previous_status The status of the post at the point where it was trashed.
4116 */
4117 $check = apply_filters( 'pre_untrash_post', null, $post, $previous_status );
4118 if ( null !== $check ) {
4119 return $check;
4120 }
4121
4122 /**
4123 * Fires before a post is restored from the Trash.
4124 *
4125 * @since 2.9.0
4126 * @since 5.6.0 Added the `$previous_status` parameter.
4127 *
4128 * @param int $post_id Post ID.
4129 * @param string $previous_status The status of the post at the point where it was trashed.
4130 */
4131 do_action( 'untrash_post', $post_id, $previous_status );
4132
4133 $new_status = ( 'attachment' === $post->post_type ) ? 'inherit' : 'draft';
4134
4135 /**
4136 * Filters the status that a post gets assigned when it is restored from the trash (untrashed).
4137 *
4138 * By default posts that are restored will be assigned a status of 'draft'. Return the value of `$previous_status`
4139 * in order to assign the status that the post had before it was trashed. The `wp_untrash_post_set_previous_status()`
4140 * function is available for this.
4141 *
4142 * Prior to WordPress 5.6.0, restored posts were always assigned their original status.
4143 *
4144 * @since 5.6.0
4145 *
4146 * @param string $new_status The new status of the post being restored.
4147 * @param int $post_id The ID of the post being restored.
4148 * @param string $previous_status The status of the post at the point where it was trashed.
4149 */
4150 $post_status = apply_filters( 'wp_untrash_post_status', $new_status, $post_id, $previous_status );
4151
4152 delete_post_meta( $post_id, '_wp_trash_meta_status' );
4153 delete_post_meta( $post_id, '_wp_trash_meta_time' );
4154
4155 $post_updated = wp_update_post(
4156 array(
4157 'ID' => $post_id,
4158 'post_status' => $post_status,
4159 )
4160 );
4161
4162 if ( ! $post_updated ) {
4163 return false;
4164 }
4165
4166 wp_untrash_post_comments( $post_id );
4167
4168 /**
4169 * Fires after a post is restored from the Trash.
4170 *
4171 * @since 2.9.0
4172 * @since 5.6.0 Added the `$previous_status` parameter.
4173 *
4174 * @param int $post_id Post ID.
4175 * @param string $previous_status The status of the post at the point where it was trashed.
4176 */
4177 do_action( 'untrashed_post', $post_id, $previous_status );
4178
4179 return $post;
4180}
4181
4182/**
4183 * Moves comments for a post to the Trash.
4184 *
4185 * @since 2.9.0
4186 *
4187 * @global wpdb $wpdb WordPress database abstraction object.
4188 *
4189 * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
4190 * @return mixed|void False on failure.
4191 */
4192function wp_trash_post_comments( $post = null ) {
4193 global $wpdb;
4194
4195 $post = get_post( $post );
4196
4197 if ( ! $post ) {
4198 return;
4199 }
4200
4201 $post_id = $post->ID;
4202
4203 /**
4204 * Fires before comments are sent to the Trash.
4205 *
4206 * @since 2.9.0
4207 *
4208 * @param int $post_id Post ID.
4209 */
4210 do_action( 'trash_post_comments', $post_id );
4211
4212 $comments = $wpdb->get_results( $wpdb->prepare( "SELECT comment_ID, comment_approved FROM $wpdb->comments WHERE comment_post_ID = %d", $post_id ) );
4213
4214 if ( ! $comments ) {
4215 return;
4216 }
4217
4218 // Cache current status for each comment.
4219 $statuses = array();
4220 foreach ( $comments as $comment ) {
4221 $statuses[ $comment->comment_ID ] = $comment->comment_approved;
4222 }
4223 add_post_meta( $post_id, '_wp_trash_meta_comments_status', $statuses );
4224
4225 // Set status for all comments to post-trashed.
4226 $result = $wpdb->update( $wpdb->comments, array( 'comment_approved' => 'post-trashed' ), array( 'comment_post_ID' => $post_id ) );
4227
4228 clean_comment_cache( array_keys( $statuses ) );
4229
4230 /**
4231 * Fires after comments are sent to the Trash.
4232 *
4233 * @since 2.9.0
4234 *
4235 * @param int $post_id Post ID.
4236 * @param array $statuses Array of comment statuses.
4237 */
4238 do_action( 'trashed_post_comments', $post_id, $statuses );
4239
4240 return $result;
4241}
4242
4243/**
4244 * Restores comments for a post from the Trash.
4245 *
4246 * @since 2.9.0
4247 *
4248 * @global wpdb $wpdb WordPress database abstraction object.
4249 *
4250 * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
4251 * @return true|void
4252 */
4253function wp_untrash_post_comments( $post = null ) {
4254 global $wpdb;
4255
4256 $post = get_post( $post );
4257
4258 if ( ! $post ) {
4259 return;
4260 }
4261
4262 $post_id = $post->ID;
4263
4264 $statuses = get_post_meta( $post_id, '_wp_trash_meta_comments_status', true );
4265
4266 if ( ! $statuses ) {
4267 return true;
4268 }
4269
4270 /**
4271 * Fires before comments are restored for a post from the Trash.
4272 *
4273 * @since 2.9.0
4274 *
4275 * @param int $post_id Post ID.
4276 */
4277 do_action( 'untrash_post_comments', $post_id );
4278
4279 // Restore each comment to its original status.
4280 $group_by_status = array();
4281 foreach ( $statuses as $comment_id => $comment_status ) {
4282 $group_by_status[ $comment_status ][] = $comment_id;
4283 }
4284
4285 foreach ( $group_by_status as $status => $comments ) {
4286 // Confidence check. This shouldn't happen.
4287 if ( 'post-trashed' === $status ) {
4288 $status = '0';
4289 }
4290 $comments_in = implode( ', ', array_map( 'intval', $comments ) );
4291 $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->comments SET comment_approved = %s WHERE comment_ID IN ($comments_in)", $status ) );
4292 }
4293
4294 clean_comment_cache( array_keys( $statuses ) );
4295
4296 delete_post_meta( $post_id, '_wp_trash_meta_comments_status' );
4297
4298 /**
4299 * Fires after comments are restored for a post from the Trash.
4300 *
4301 * @since 2.9.0
4302 *
4303 * @param int $post_id Post ID.
4304 */
4305 do_action( 'untrashed_post_comments', $post_id );
4306}
4307
4308/**
4309 * Retrieves the list of categories for a post.
4310 *
4311 * Compatibility layer for themes and plugins. Also an easy layer of abstraction
4312 * away from the complexity of the taxonomy layer.
4313 *
4314 * @since 2.1.0
4315 *
4316 * @see wp_get_object_terms()
4317 *
4318 * @param int $post_id Optional. The Post ID. Does not default to the ID of the
4319 * global $post. Default 0.
4320 * @param array $args Optional. Category query parameters. Default empty array.
4321 * See WP_Term_Query::__construct() for supported arguments.
4322 * @return array|WP_Error List of categories. If the `$fields` argument passed via `$args` is 'all' or
4323 * 'all_with_object_id', an array of WP_Term objects will be returned. If `$fields`
4324 * is 'ids', an array of category IDs. If `$fields` is 'names', an array of category names.
4325 * WP_Error object if 'category' taxonomy doesn't exist.
4326 */
4327function wp_get_post_categories( $post_id = 0, $args = array() ) {
4328 $post_id = (int) $post_id;
4329
4330 $defaults = array( 'fields' => 'ids' );
4331 $args = wp_parse_args( $args, $defaults );
4332
4333 $cats = wp_get_object_terms( $post_id, 'category', $args );
4334 return $cats;
4335}
4336
4337/**
4338 * Retrieves the tags for a post.
4339 *
4340 * There is only one default for this function, called 'fields' and by default
4341 * is set to 'all'. There are other defaults that can be overridden in
4342 * wp_get_object_terms().
4343 *
4344 * @since 2.3.0
4345 *
4346 * @param int $post_id Optional. The Post ID. Does not default to the ID of the
4347 * global $post. Default 0.
4348 * @param array $args Optional. Tag query parameters. Default empty array.
4349 * See WP_Term_Query::__construct() for supported arguments.
4350 * @return array|WP_Error Array of WP_Term objects on success or empty array if no tags were found.
4351 * WP_Error object if 'post_tag' taxonomy doesn't exist.
4352 */
4353function wp_get_post_tags( $post_id = 0, $args = array() ) {
4354 return wp_get_post_terms( $post_id, 'post_tag', $args );
4355}
4356
4357/**
4358 * Retrieves the terms for a post.
4359 *
4360 * @since 2.8.0
4361 *
4362 * @param int $post_id Optional. The Post ID. Does not default to the ID of the
4363 * global $post. Default 0.
4364 * @param string|string[] $taxonomy Optional. The taxonomy slug or array of slugs for which
4365 * to retrieve terms. Default 'post_tag'.
4366 * @param array $args {
4367 * Optional. Term query parameters. See WP_Term_Query::__construct() for supported arguments.
4368 *
4369 * @type string $fields Term fields to retrieve. Default 'all'.
4370 * }
4371 * @return array|WP_Error Array of WP_Term objects on success or empty array if no terms were found.
4372 * WP_Error object if `$taxonomy` doesn't exist.
4373 */
4374function wp_get_post_terms( $post_id = 0, $taxonomy = 'post_tag', $args = array() ) {
4375 $post_id = (int) $post_id;
4376
4377 $defaults = array( 'fields' => 'all' );
4378 $args = wp_parse_args( $args, $defaults );
4379
4380 $tags = wp_get_object_terms( $post_id, $taxonomy, $args );
4381
4382 return $tags;
4383}
4384
4385/**
4386 * Retrieves a number of recent posts.
4387 *
4388 * @since 1.0.0
4389 *
4390 * @see get_posts()
4391 *
4392 * @param array $args Optional. Arguments to retrieve posts. Default empty array.
4393 * @param string $output Optional. The required return type. One of OBJECT or ARRAY_A, which
4394 * correspond to a WP_Post object or an associative array, respectively.
4395 * Default ARRAY_A.
4396 * @return array|false Array of recent posts, where the type of each element is determined
4397 * by the `$output` parameter. Empty array on failure.
4398 */
4399function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) {
4400
4401 if ( is_numeric( $args ) ) {
4402 _deprecated_argument( __FUNCTION__, '3.1.0', __( 'Passing an integer number of posts is deprecated. Pass an array of arguments instead.' ) );
4403 $args = array( 'numberposts' => absint( $args ) );
4404 }
4405
4406 // Set default arguments.
4407 $defaults = array(
4408 'numberposts' => 10,
4409 'offset' => 0,
4410 'category' => 0,
4411 'orderby' => 'post_date',
4412 'order' => 'DESC',
4413 'include' => '',
4414 'exclude' => '',
4415 'meta_key' => '',
4416 'meta_value' => '',
4417 'post_type' => 'post',
4418 'post_status' => 'draft, publish, future, pending, private',
4419 'suppress_filters' => true,
4420 );
4421
4422 $parsed_args = wp_parse_args( $args, $defaults );
4423
4424 $results = get_posts( $parsed_args );
4425
4426 // Backward compatibility. Prior to 3.1 expected posts to be returned in array.
4427 if ( ARRAY_A === $output ) {
4428 foreach ( $results as $key => $result ) {
4429 $results[ $key ] = get_object_vars( $result );
4430 }
4431 return $results ? $results : array();
4432 }
4433
4434 return $results ? $results : false;
4435}
4436
4437/**
4438 * Inserts or update a post.
4439 *
4440 * If the $postarr parameter has 'ID' set to a value, then post will be updated.
4441 *
4442 * You can set the post date manually, by setting the values for 'post_date'
4443 * and 'post_date_gmt' keys. You can close the comments or open the comments by
4444 * setting the value for 'comment_status' key.
4445 *
4446 * @since 1.0.0
4447 * @since 2.6.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure.
4448 * @since 4.2.0 Support was added for encoding emoji in the post title, content, and excerpt.
4449 * @since 4.4.0 A 'meta_input' array can now be passed to `$postarr` to add post meta data.
4450 * @since 5.6.0 Added the `$fire_after_hooks` parameter.
4451 *
4452 * @see sanitize_post()
4453 * @global wpdb $wpdb WordPress database abstraction object.
4454 *
4455 * @param array $postarr {
4456 * An array of elements that make up a post to update or insert.
4457 *
4458 * @type int $ID The post ID. If equal to something other than 0,
4459 * the post with that ID will be updated. Default 0.
4460 * @type int $post_author The ID of the user who added the post. Default is
4461 * the current user ID.
4462 * @type string $post_date The date of the post. Default is the current time.
4463 * @type string $post_date_gmt The date of the post in the GMT timezone. Default is
4464 * the value of `$post_date`.
4465 * @type string $post_content The post content. Default empty.
4466 * @type string $post_content_filtered The filtered post content. Default empty.
4467 * @type string $post_title The post title. Default empty.
4468 * @type string $post_excerpt The post excerpt. Default empty.
4469 * @type string $post_status The post status. Default 'draft'.
4470 * @type string $post_type The post type. Default 'post'.
4471 * @type string $comment_status Whether the post can accept comments. Accepts 'open' or 'closed'.
4472 * Default is the value of 'default_comment_status' option.
4473 * @type string $ping_status Whether the post can accept pings. Accepts 'open' or 'closed'.
4474 * Default is the value of 'default_ping_status' option.
4475 * @type string $post_password The password to access the post. Default empty.
4476 * @type string $post_name The post name. Default is the sanitized post title
4477 * when creating a new post.
4478 * @type string $to_ping Space or carriage return-separated list of URLs to ping.
4479 * Default empty.
4480 * @type string $pinged Space or carriage return-separated list of URLs that have
4481 * been pinged. Default empty.
4482 * @type int $post_parent Set this for the post it belongs to, if any. Default 0.
4483 * @type int $menu_order The order the post should be displayed in. Default 0.
4484 * @type string $post_mime_type The mime type of the post. Default empty.
4485 * @type string $guid Global Unique ID for referencing the post. Default empty.
4486 * @type int $import_id The post ID to be used when inserting a new post.
4487 * If specified, must not match any existing post ID. Default 0.
4488 * @type int[] $post_category Array of category IDs.
4489 * Defaults to value of the 'default_category' option.
4490 * @type array $tags_input Array of tag names, slugs, or IDs. Default empty.
4491 * @type array $tax_input An array of taxonomy terms keyed by their taxonomy name.
4492 * If the taxonomy is hierarchical, the term list needs to be
4493 * either an array of term IDs or a comma-separated string of IDs.
4494 * If the taxonomy is non-hierarchical, the term list can be an array
4495 * that contains term names or slugs, or a comma-separated string
4496 * of names or slugs. This is because, in hierarchical taxonomy,
4497 * child terms can have the same names with different parent terms,
4498 * so the only way to connect them is using ID. Default empty.
4499 * @type array $meta_input Array of post meta values keyed by their post meta key. Default empty.
4500 * @type string $page_template Page template to use.
4501 * }
4502 * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false.
4503 * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true.
4504 * @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure.
4505 */
4506function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) {
4507 global $wpdb;
4508
4509 // Capture original pre-sanitized array for passing into filters.
4510 $unsanitized_postarr = $postarr;
4511
4512 $user_id = get_current_user_id();
4513
4514 $defaults = array(
4515 'post_author' => $user_id,
4516 'post_content' => '',
4517 'post_content_filtered' => '',
4518 'post_title' => '',
4519 'post_excerpt' => '',
4520 'post_status' => 'draft',
4521 'post_type' => 'post',
4522 'comment_status' => '',
4523 'ping_status' => '',
4524 'post_password' => '',
4525 'to_ping' => '',
4526 'pinged' => '',
4527 'post_parent' => 0,
4528 'menu_order' => 0,
4529 'guid' => '',
4530 'import_id' => 0,
4531 'context' => '',
4532 'post_date' => '',
4533 'post_date_gmt' => '',
4534 );
4535
4536 $postarr = wp_parse_args( $postarr, $defaults );
4537
4538 unset( $postarr['filter'] );
4539
4540 $postarr = sanitize_post( $postarr, 'db' );
4541
4542 // Are we updating or creating?
4543 $post_id = 0;
4544 $update = false;
4545 $guid = $postarr['guid'];
4546
4547 if ( ! empty( $postarr['ID'] ) ) {
4548 $update = true;
4549
4550 // Get the post ID and GUID.
4551 $post_id = $postarr['ID'];
4552 $post_before = get_post( $post_id );
4553
4554 if ( is_null( $post_before ) ) {
4555 if ( $wp_error ) {
4556 return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
4557 }
4558 return 0;
4559 }
4560
4561 $guid = get_post_field( 'guid', $post_id );
4562 $previous_status = get_post_field( 'post_status', $post_id );
4563 } else {
4564 $previous_status = 'new';
4565 $post_before = null;
4566 }
4567
4568 $post_type = empty( $postarr['post_type'] ) ? 'post' : $postarr['post_type'];
4569
4570 $post_title = $postarr['post_title'];
4571 $post_content = $postarr['post_content'];
4572 $post_excerpt = $postarr['post_excerpt'];
4573
4574 if ( isset( $postarr['post_name'] ) ) {
4575 $post_name = $postarr['post_name'];
4576 } elseif ( $update ) {
4577 // For an update, don't modify the post_name if it wasn't supplied as an argument.
4578 $post_name = $post_before->post_name;
4579 }
4580
4581 $maybe_empty = 'attachment' !== $post_type
4582 && ! $post_content && ! $post_title && ! $post_excerpt
4583 && post_type_supports( $post_type, 'editor' )
4584 && post_type_supports( $post_type, 'title' )
4585 && post_type_supports( $post_type, 'excerpt' );
4586
4587 /**
4588 * Filters whether the post should be considered "empty".
4589 *
4590 * The post is considered "empty" if both:
4591 * 1. The post type supports the title, editor, and excerpt fields
4592 * 2. The title, editor, and excerpt fields are all empty
4593 *
4594 * Returning a truthy value from the filter will effectively short-circuit
4595 * the new post being inserted and return 0. If $wp_error is true, a WP_Error
4596 * will be returned instead.
4597 *
4598 * @since 3.3.0
4599 *
4600 * @param bool $maybe_empty Whether the post should be considered "empty".
4601 * @param array $postarr Array of post data.
4602 */
4603 if ( apply_filters( 'wp_insert_post_empty_content', $maybe_empty, $postarr ) ) {
4604 if ( $wp_error ) {
4605 return new WP_Error( 'empty_content', __( 'Content, title, and excerpt are empty.' ) );
4606 } else {
4607 return 0;
4608 }
4609 }
4610
4611 $post_status = empty( $postarr['post_status'] ) ? 'draft' : $postarr['post_status'];
4612
4613 if ( 'attachment' === $post_type && ! in_array( $post_status, array( 'inherit', 'private', 'trash', 'auto-draft' ), true ) ) {
4614 $post_status = 'inherit';
4615 }
4616
4617 if ( ! empty( $postarr['post_category'] ) ) {
4618 // Filter out empty terms.
4619 $post_category = array_filter( $postarr['post_category'] );
4620 } elseif ( $update && ! isset( $postarr['post_category'] ) ) {
4621 $post_category = $post_before->post_category;
4622 }
4623
4624 // Make sure we set a valid category.
4625 if ( empty( $post_category ) || 0 === count( $post_category ) || ! is_array( $post_category ) ) {
4626 // 'post' requires at least one category.
4627 if ( 'post' === $post_type && 'auto-draft' !== $post_status ) {
4628 $post_category = array( get_option( 'default_category' ) );
4629 } else {
4630 $post_category = array();
4631 }
4632 }
4633
4634 /*
4635 * Don't allow contributors to set the post slug for pending review posts.
4636 *
4637 * For new posts check the primitive capability, for updates check the meta capability.
4638 */
4639 if ( 'pending' === $post_status ) {
4640 $post_type_object = get_post_type_object( $post_type );
4641
4642 if ( ! $update && $post_type_object && ! current_user_can( $post_type_object->cap->publish_posts ) ) {
4643 $post_name = '';
4644 } elseif ( $update && ! current_user_can( 'publish_post', $post_id ) ) {
4645 $post_name = '';
4646 }
4647 }
4648
4649 /*
4650 * Create a valid post name. Drafts and pending posts are allowed to have
4651 * an empty post name.
4652 */
4653 if ( empty( $post_name ) ) {
4654 if ( ! in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) ) {
4655 $post_name = sanitize_title( $post_title );
4656 } else {
4657 $post_name = '';
4658 }
4659 } else {
4660 // On updates, we need to check to see if it's using the old, fixed sanitization context.
4661 $check_name = sanitize_title( $post_name, '', 'old-save' );
4662
4663 if ( $update
4664 && strtolower( urlencode( $post_name ) ) === $check_name
4665 && get_post_field( 'post_name', $post_id ) === $check_name
4666 ) {
4667 $post_name = $check_name;
4668 } else { // New post, or slug has changed.
4669 $post_name = sanitize_title( $post_name );
4670 }
4671 }
4672
4673 /*
4674 * Resolve the post date from any provided post date or post date GMT strings;
4675 * if none are provided, the date will be set to now.
4676 */
4677 $post_date = wp_resolve_post_date( $postarr['post_date'], $postarr['post_date_gmt'] );
4678
4679 if ( ! $post_date ) {
4680 if ( $wp_error ) {
4681 return new WP_Error( 'invalid_date', __( 'Invalid date.' ) );
4682 } else {
4683 return 0;
4684 }
4685 }
4686
4687 if ( empty( $postarr['post_date_gmt'] ) || '0000-00-00 00:00:00' === $postarr['post_date_gmt'] ) {
4688 if ( ! in_array( $post_status, get_post_stati( array( 'date_floating' => true ) ), true ) ) {
4689 $post_date_gmt = get_gmt_from_date( $post_date );
4690 } else {
4691 $post_date_gmt = '0000-00-00 00:00:00';
4692 }
4693 } else {
4694 $post_date_gmt = $postarr['post_date_gmt'];
4695 }
4696
4697 if ( $update || '0000-00-00 00:00:00' === $post_date ) {
4698 $post_modified = current_time( 'mysql' );
4699 $post_modified_gmt = current_time( 'mysql', true );
4700 } else {
4701 $post_modified = $post_date;
4702 $post_modified_gmt = $post_date_gmt;
4703 }
4704
4705 if ( 'attachment' !== $post_type ) {
4706 $now = gmdate( 'Y-m-d H:i:s' );
4707
4708 if ( 'publish' === $post_status ) {
4709 if ( strtotime( $post_date_gmt ) - strtotime( $now ) >= MINUTE_IN_SECONDS ) {
4710 $post_status = 'future';
4711 }
4712 } elseif ( 'future' === $post_status ) {
4713 if ( strtotime( $post_date_gmt ) - strtotime( $now ) < MINUTE_IN_SECONDS ) {
4714 $post_status = 'publish';
4715 }
4716 }
4717 }
4718
4719 // Comment status.
4720 if ( empty( $postarr['comment_status'] ) ) {
4721 if ( $update ) {
4722 $comment_status = 'closed';
4723 } else {
4724 $comment_status = get_default_comment_status( $post_type );
4725 }
4726 } else {
4727 $comment_status = $postarr['comment_status'];
4728 }
4729
4730 // These variables are needed by compact() later.
4731 $post_content_filtered = $postarr['post_content_filtered'];
4732 $post_author = isset( $postarr['post_author'] ) ? $postarr['post_author'] : $user_id;
4733 $ping_status = empty( $postarr['ping_status'] ) ? get_default_comment_status( $post_type, 'pingback' ) : $postarr['ping_status'];
4734 $to_ping = isset( $postarr['to_ping'] ) ? sanitize_trackback_urls( $postarr['to_ping'] ) : '';
4735 $pinged = isset( $postarr['pinged'] ) ? $postarr['pinged'] : '';
4736 $import_id = isset( $postarr['import_id'] ) ? $postarr['import_id'] : 0;
4737
4738 /*
4739 * The 'wp_insert_post_parent' filter expects all variables to be present.
4740 * Previously, these variables would have already been extracted
4741 */
4742 if ( isset( $postarr['menu_order'] ) ) {
4743 $menu_order = (int) $postarr['menu_order'];
4744 } else {
4745 $menu_order = 0;
4746 }
4747
4748 $post_password = isset( $postarr['post_password'] ) ? $postarr['post_password'] : '';
4749 if ( 'private' === $post_status ) {
4750 $post_password = '';
4751 }
4752
4753 if ( isset( $postarr['post_parent'] ) ) {
4754 $post_parent = (int) $postarr['post_parent'];
4755 } else {
4756 $post_parent = 0;
4757 }
4758
4759 $new_postarr = array_merge(
4760 array(
4761 'ID' => $post_id,
4762 ),
4763 compact( array_diff( array_keys( $defaults ), array( 'context', 'filter' ) ) )
4764 );
4765
4766 /**
4767 * Filters the post parent -- used to check for and prevent hierarchy loops.
4768 *
4769 * @since 3.1.0
4770 *
4771 * @param int $post_parent Post parent ID.
4772 * @param int $post_id Post ID.
4773 * @param array $new_postarr Array of parsed post data.
4774 * @param array $postarr Array of sanitized, but otherwise unmodified post data.
4775 */
4776 $post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_id, $new_postarr, $postarr );
4777
4778 /*
4779 * If the post is being untrashed and it has a desired slug stored in post meta,
4780 * reassign it.
4781 */
4782 if ( 'trash' === $previous_status && 'trash' !== $post_status ) {
4783 $desired_post_slug = get_post_meta( $post_id, '_wp_desired_post_slug', true );
4784
4785 if ( $desired_post_slug ) {
4786 delete_post_meta( $post_id, '_wp_desired_post_slug' );
4787 $post_name = $desired_post_slug;
4788 }
4789 }
4790
4791 // If a trashed post has the desired slug, change it and let this post have it.
4792 if ( 'trash' !== $post_status && $post_name ) {
4793 /**
4794 * Filters whether or not to add a `__trashed` suffix to trashed posts that match the name of the updated post.
4795 *
4796 * @since 5.4.0
4797 *
4798 * @param bool $add_trashed_suffix Whether to attempt to add the suffix.
4799 * @param string $post_name The name of the post being updated.
4800 * @param int $post_id Post ID.
4801 */
4802 $add_trashed_suffix = apply_filters( 'add_trashed_suffix_to_trashed_posts', true, $post_name, $post_id );
4803
4804 if ( $add_trashed_suffix ) {
4805 wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id );
4806 }
4807 }
4808
4809 // When trashing an existing post, change its slug to allow non-trashed posts to use it.
4810 if ( 'trash' === $post_status && 'trash' !== $previous_status && 'new' !== $previous_status ) {
4811 $post_name = wp_add_trashed_suffix_to_post_name_for_post( $post_id );
4812 }
4813
4814 $post_name = wp_unique_post_slug( $post_name, $post_id, $post_status, $post_type, $post_parent );
4815
4816 // Don't unslash.
4817 $post_mime_type = isset( $postarr['post_mime_type'] ) ? $postarr['post_mime_type'] : '';
4818
4819 // Expected_slashed (everything!).
4820 $data = compact(
4821 'post_author',
4822 'post_date',
4823 'post_date_gmt',
4824 'post_content',
4825 'post_content_filtered',
4826 'post_title',
4827 'post_excerpt',
4828 'post_status',
4829 'post_type',
4830 'comment_status',
4831 'ping_status',
4832 'post_password',
4833 'post_name',
4834 'to_ping',
4835 'pinged',
4836 'post_modified',
4837 'post_modified_gmt',
4838 'post_parent',
4839 'menu_order',
4840 'post_mime_type',
4841 'guid'
4842 );
4843
4844 $emoji_fields = array( 'post_title', 'post_content', 'post_excerpt' );
4845
4846 foreach ( $emoji_fields as $emoji_field ) {
4847 if ( isset( $data[ $emoji_field ] ) ) {
4848 $charset = $wpdb->get_col_charset( $wpdb->posts, $emoji_field );
4849
4850 // The 'utf8' character set is a deprecated alias of 'utf8mb3'. See <https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8.html>.
4851 if ( 'utf8' === $charset || 'utf8mb3' === $charset ) {
4852 $data[ $emoji_field ] = wp_encode_emoji( $data[ $emoji_field ] );
4853 }
4854 }
4855 }
4856
4857 if ( 'attachment' === $post_type ) {
4858 /**
4859 * Filters attachment post data before it is updated in or added to the database.
4860 *
4861 * @since 3.9.0
4862 * @since 5.4.1 The `$unsanitized_postarr` parameter was added.
4863 * @since 6.0.0 The `$update` parameter was added.
4864 *
4865 * @param array $data An array of slashed, sanitized, and processed attachment post data.
4866 * @param array $postarr An array of slashed and sanitized attachment post data, but not processed.
4867 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data
4868 * as originally passed to wp_insert_post().
4869 * @param bool $update Whether this is an existing attachment post being updated.
4870 */
4871 $data = apply_filters( 'wp_insert_attachment_data', $data, $postarr, $unsanitized_postarr, $update );
4872 } else {
4873 /**
4874 * Filters slashed post data just before it is inserted into the database.
4875 *
4876 * @since 2.7.0
4877 * @since 5.4.1 The `$unsanitized_postarr` parameter was added.
4878 * @since 6.0.0 The `$update` parameter was added.
4879 *
4880 * @param array $data An array of slashed, sanitized, and processed post data.
4881 * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
4882 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as
4883 * originally passed to wp_insert_post().
4884 * @param bool $update Whether this is an existing post being updated.
4885 */
4886 $data = apply_filters( 'wp_insert_post_data', $data, $postarr, $unsanitized_postarr, $update );
4887 }
4888
4889 $data = wp_unslash( $data );
4890 $where = array( 'ID' => $post_id );
4891
4892 if ( $update ) {
4893 /**
4894 * Fires immediately before an existing post is updated in the database.
4895 *
4896 * @since 2.5.0
4897 *
4898 * @param int $post_id Post ID.
4899 * @param array $data Array of unslashed post data.
4900 */
4901 do_action( 'pre_post_update', $post_id, $data );
4902
4903 if ( false === $wpdb->update( $wpdb->posts, $data, $where ) ) {
4904 if ( $wp_error ) {
4905 if ( 'attachment' === $post_type ) {
4906 $message = __( 'Could not update attachment in the database.' );
4907 } else {
4908 $message = __( 'Could not update post in the database.' );
4909 }
4910
4911 return new WP_Error( 'db_update_error', $message, $wpdb->last_error );
4912 } else {
4913 return 0;
4914 }
4915 }
4916 } else {
4917 // If there is a suggested ID, use it if not already present.
4918 if ( ! empty( $import_id ) ) {
4919 $import_id = (int) $import_id;
4920
4921 if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE ID = %d", $import_id ) ) ) {
4922 $data['ID'] = $import_id;
4923 }
4924 }
4925
4926 /**
4927 * Fires immediately before a new post is inserted in the database.
4928 *
4929 * @since 6.9.0
4930 *
4931 * @param array $data Array of unslashed post data.
4932 */
4933 do_action( 'pre_post_insert', $data );
4934
4935 if ( false === $wpdb->insert( $wpdb->posts, $data ) ) {
4936 if ( $wp_error ) {
4937 if ( 'attachment' === $post_type ) {
4938 $message = __( 'Could not insert attachment into the database.' );
4939 } else {
4940 $message = __( 'Could not insert post into the database.' );
4941 }
4942
4943 return new WP_Error( 'db_insert_error', $message, $wpdb->last_error );
4944 } else {
4945 return 0;
4946 }
4947 }
4948
4949 $post_id = (int) $wpdb->insert_id;
4950
4951 // Use the newly generated $post_id.
4952 $where = array( 'ID' => $post_id );
4953 }
4954
4955 if ( empty( $data['post_name'] ) && ! in_array( $data['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) ) {
4956 $data['post_name'] = wp_unique_post_slug( sanitize_title( $data['post_title'], $post_id ), $post_id, $data['post_status'], $post_type, $post_parent );
4957
4958 $wpdb->update( $wpdb->posts, array( 'post_name' => $data['post_name'] ), $where );
4959 clean_post_cache( $post_id );
4960 }
4961
4962 if ( is_object_in_taxonomy( $post_type, 'category' ) ) {
4963 wp_set_post_categories( $post_id, $post_category );
4964 }
4965
4966 if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $post_type, 'post_tag' ) ) {
4967 wp_set_post_tags( $post_id, $postarr['tags_input'] );
4968 }
4969
4970 // Add default term for all associated custom taxonomies.
4971 if ( 'auto-draft' !== $post_status ) {
4972 foreach ( get_object_taxonomies( $post_type, 'object' ) as $taxonomy => $tax_object ) {
4973
4974 if ( ! empty( $tax_object->default_term ) ) {
4975
4976 // Filter out empty terms.
4977 if ( isset( $postarr['tax_input'][ $taxonomy ] ) && is_array( $postarr['tax_input'][ $taxonomy ] ) ) {
4978 $postarr['tax_input'][ $taxonomy ] = array_filter( $postarr['tax_input'][ $taxonomy ] );
4979 }
4980
4981 // Passed custom taxonomy list overwrites the existing list if not empty.
4982 $terms = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
4983 if ( ! empty( $terms ) && empty( $postarr['tax_input'][ $taxonomy ] ) ) {
4984 $postarr['tax_input'][ $taxonomy ] = $terms;
4985 }
4986
4987 if ( empty( $postarr['tax_input'][ $taxonomy ] ) ) {
4988 $default_term_id = get_option( 'default_term_' . $taxonomy );
4989 if ( ! empty( $default_term_id ) ) {
4990 $postarr['tax_input'][ $taxonomy ] = array( (int) $default_term_id );
4991 }
4992 }
4993 }
4994 }
4995 }
4996
4997 // New-style support for all custom taxonomies.
4998 if ( ! empty( $postarr['tax_input'] ) ) {
4999 foreach ( $postarr['tax_input'] as $taxonomy => $tags ) {
5000 $taxonomy_obj = get_taxonomy( $taxonomy );
5001
5002 if ( ! $taxonomy_obj ) {
5003 /* translators: %s: Taxonomy name. */
5004 _doing_it_wrong( __FUNCTION__, sprintf( __( 'Invalid taxonomy: %s.' ), $taxonomy ), '4.4.0' );
5005 continue;
5006 }
5007
5008 // array = hierarchical, string = non-hierarchical.
5009 if ( is_array( $tags ) ) {
5010 $tags = array_filter( $tags );
5011 }
5012
5013 if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
5014 wp_set_post_terms( $post_id, $tags, $taxonomy );
5015 }
5016 }
5017 }
5018
5019 if ( ! empty( $postarr['meta_input'] ) ) {
5020 foreach ( $postarr['meta_input'] as $field => $value ) {
5021 update_post_meta( $post_id, $field, $value );
5022 }
5023 }
5024
5025 $current_guid = get_post_field( 'guid', $post_id );
5026
5027 // Set GUID.
5028 if ( ! $update && '' === $current_guid ) {
5029 $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post_id ) ), $where );
5030 }
5031
5032 if ( 'attachment' === $postarr['post_type'] ) {
5033 if ( ! empty( $postarr['file'] ) ) {
5034 update_attached_file( $post_id, $postarr['file'] );
5035 }
5036
5037 if ( ! empty( $postarr['context'] ) ) {
5038 add_post_meta( $post_id, '_wp_attachment_context', $postarr['context'], true );
5039 }
5040 }
5041
5042 // Set or remove featured image.
5043 if ( isset( $postarr['_thumbnail_id'] ) ) {
5044 $thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ) || 'revision' === $post_type;
5045
5046 if ( ! $thumbnail_support && 'attachment' === $post_type && $post_mime_type ) {
5047 if ( wp_attachment_is( 'audio', $post_id ) ) {
5048 $thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' );
5049 } elseif ( wp_attachment_is( 'video', $post_id ) ) {
5050 $thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' );
5051 }
5052 }
5053
5054 if ( $thumbnail_support ) {
5055 $thumbnail_id = (int) $postarr['_thumbnail_id'];
5056 if ( -1 === $thumbnail_id ) {
5057 delete_post_thumbnail( $post_id );
5058 } else {
5059 set_post_thumbnail( $post_id, $thumbnail_id );
5060 }
5061 }
5062 }
5063
5064 clean_post_cache( $post_id );
5065
5066 do_action('godaddy/wp_insert_post/before_get_post_instance');
5067 $post = get_post( $post_id );
5068 do_action('godaddy/wp_insert_post/after_get_post_instance');
5069
5070 if ( ! empty( $postarr['page_template'] ) ) {
5071 $post->page_template = $postarr['page_template'];
5072 $page_templates = wp_get_theme()->get_page_templates( $post );
5073
5074 if ( 'default' !== $postarr['page_template'] && ! isset( $page_templates[ $postarr['page_template'] ] ) ) {
5075 if ( $wp_error ) {
5076 return new WP_Error( 'invalid_page_template', __( 'Invalid page template.' ) );
5077 }
5078
5079 update_post_meta( $post_id, '_wp_page_template', 'default' );
5080 } else {
5081 update_post_meta( $post_id, '_wp_page_template', $postarr['page_template'] );
5082 }
5083 }
5084
5085 if ( 'attachment' !== $postarr['post_type'] ) {
5086 wp_transition_post_status( $data['post_status'], $previous_status, $post );
5087 } else {
5088 if ( $update ) {
5089 /**
5090 * Fires once an existing attachment has been updated.
5091 *
5092 * @since 2.0.0
5093 *
5094 * @param int $post_id Attachment ID.
5095 */
5096 do_action( 'edit_attachment', $post_id );
5097
5098 $post_after = get_post( $post_id );
5099
5100 /**
5101 * Fires once an existing attachment has been updated.
5102 *
5103 * @since 4.4.0
5104 *
5105 * @param int $post_id Post ID.
5106 * @param WP_Post $post_after Post object following the update.
5107 * @param WP_Post $post_before Post object before the update.
5108 */
5109 do_action( 'attachment_updated', $post_id, $post_after, $post_before );
5110 } else {
5111
5112 /**
5113 * Fires once an attachment has been added.
5114 *
5115 * @since 2.0.0
5116 *
5117 * @param int $post_id Attachment ID.
5118 */
5119 do_action( 'add_attachment', $post_id );
5120 }
5121
5122 return $post_id;
5123 }
5124
5125 if ( $update ) {
5126 /**
5127 * Fires once an existing post has been updated.
5128 *
5129 * The dynamic portion of the hook name, `$post->post_type`, refers to
5130 * the post type slug.
5131 *
5132 * Possible hook names include:
5133 *
5134 * - `edit_post_post`
5135 * - `edit_post_page`
5136 *
5137 * @since 5.1.0
5138 *
5139 * @param int $post_id Post ID.
5140 * @param WP_Post $post Post object.
5141 */
5142 do_action( "edit_post_{$post->post_type}", $post_id, $post );
5143
5144 /**
5145 * Fires once an existing post has been updated.
5146 *
5147 * @since 1.2.0
5148 *
5149 * @param int $post_id Post ID.
5150 * @param WP_Post $post Post object.
5151 */
5152 do_action( 'edit_post', $post_id, $post );
5153
5154 $post_after = get_post( $post_id );
5155
5156 /**
5157 * Fires once an existing post has been updated.
5158 *
5159 * @since 3.0.0
5160 *
5161 * @param int $post_id Post ID.
5162 * @param WP_Post $post_after Post object following the update.
5163 * @param WP_Post $post_before Post object before the update.
5164 */
5165 do_action( 'post_updated', $post_id, $post_after, $post_before );
5166 }
5167
5168 /**
5169 * Fires once a post has been saved.
5170 *
5171 * The dynamic portion of the hook name, `$post->post_type`, refers to
5172 * the post type slug.
5173 *
5174 * Possible hook names include:
5175 *
5176 * - `save_post_post`
5177 * - `save_post_page`
5178 *
5179 * @since 3.7.0
5180 *
5181 * @param int $post_id Post ID.
5182 * @param WP_Post $post Post object.
5183 * @param bool $update Whether this is an existing post being updated.
5184 */
5185 do_action( "save_post_{$post->post_type}", $post_id, $post, $update );
5186
5187 /**
5188 * Fires once a post has been saved.
5189 *
5190 * @since 1.5.0
5191 *
5192 * @param int $post_id Post ID.
5193 * @param WP_Post $post Post object.
5194 * @param bool $update Whether this is an existing post being updated.
5195 */
5196 do_action( 'save_post', $post_id, $post, $update );
5197
5198 /**
5199 * Fires once a post has been saved.
5200 *
5201 * @since 2.0.0
5202 *
5203 * @param int $post_id Post ID.
5204 * @param WP_Post $post Post object.
5205 * @param bool $update Whether this is an existing post being updated.
5206 */
5207 do_action( 'wp_insert_post', $post_id, $post, $update );
5208
5209 if ( $fire_after_hooks ) {
5210 wp_after_insert_post( $post, $update, $post_before );
5211 }
5212
5213 return $post_id;
5214}
5215
5216/**
5217 * Updates a post with new post data.
5218 *
5219 * The date does not have to be set for drafts. You can set the date and it will
5220 * not be overridden.
5221 *
5222 * @since 1.0.0
5223 * @since 3.5.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure.
5224 * @since 5.6.0 Added the `$fire_after_hooks` parameter.
5225 *
5226 * @param array|object $postarr Optional. Post data. Arrays are expected to be escaped,
5227 * objects are not. See wp_insert_post() for accepted arguments.
5228 * Default array.
5229 * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false.
5230 * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true.
5231 * @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure.
5232 */
5233function wp_update_post( $postarr = array(), $wp_error = false, $fire_after_hooks = true ) {
5234 if ( is_object( $postarr ) ) {
5235 // Non-escaped post was passed.
5236 $postarr = get_object_vars( $postarr );
5237 $postarr = wp_slash( $postarr );
5238 }
5239
5240 // First, get all of the original fields.
5241 $post = get_post( $postarr['ID'], ARRAY_A );
5242
5243 if ( is_null( $post ) ) {
5244 if ( $wp_error ) {
5245 return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
5246 }
5247 return 0;
5248 }
5249
5250 // Escape data pulled from DB.
5251 $post = wp_slash( $post );
5252
5253 // Passed post category list overwrites existing category list if not empty.
5254 if ( isset( $postarr['post_category'] ) && is_array( $postarr['post_category'] )
5255 && count( $postarr['post_category'] ) > 0
5256 ) {
5257 $post_cats = $postarr['post_category'];
5258 } else {
5259 $post_cats = $post['post_category'];
5260 }
5261
5262 // Drafts shouldn't be assigned a date unless explicitly done so by the user.
5263 if ( isset( $post['post_status'] )
5264 && in_array( $post['post_status'], array( 'draft', 'pending', 'auto-draft' ), true )
5265 && empty( $postarr['edit_date'] ) && ( '0000-00-00 00:00:00' === $post['post_date_gmt'] )
5266 ) {
5267 $clear_date = true;
5268 } else {
5269 $clear_date = false;
5270 }
5271
5272 // Merge old and new fields with new fields overwriting old ones.
5273 $postarr = array_merge( $post, $postarr );
5274 $postarr['post_category'] = $post_cats;
5275 if ( $clear_date ) {
5276 $postarr['post_date'] = current_time( 'mysql' );
5277 $postarr['post_date_gmt'] = '';
5278 }
5279
5280 if ( 'attachment' === $postarr['post_type'] ) {
5281 return wp_insert_attachment( $postarr, false, 0, $wp_error );
5282 }
5283
5284 // Discard 'tags_input' parameter if it's the same as existing post tags.
5285 if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $postarr['post_type'], 'post_tag' ) ) {
5286 $tags = get_the_terms( $postarr['ID'], 'post_tag' );
5287 $tag_names = array();
5288
5289 if ( $tags && ! is_wp_error( $tags ) ) {
5290 $tag_names = wp_list_pluck( $tags, 'name' );
5291 }
5292
5293 if ( $postarr['tags_input'] === $tag_names ) {
5294 unset( $postarr['tags_input'] );
5295 }
5296 }
5297
5298 return wp_insert_post( $postarr, $wp_error, $fire_after_hooks );
5299}
5300
5301/**
5302 * Publishes a post by transitioning the post status.
5303 *
5304 * @since 2.1.0
5305 *
5306 * @global wpdb $wpdb WordPress database abstraction object.
5307 *
5308 * @param int|WP_Post $post Post ID or post object.
5309 */
5310function wp_publish_post( $post ) {
5311 global $wpdb;
5312
5313 $post = get_post( $post );
5314
5315 if ( ! $post ) {
5316 return;
5317 }
5318
5319 if ( 'publish' === $post->post_status ) {
5320 return;
5321 }
5322
5323 $post_before = get_post( $post->ID );
5324
5325 // Ensure at least one term is applied for taxonomies with a default term.
5326 foreach ( get_object_taxonomies( $post->post_type, 'object' ) as $taxonomy => $tax_object ) {
5327 // Skip taxonomy if no default term is set.
5328 if (
5329 'category' !== $taxonomy &&
5330 empty( $tax_object->default_term )
5331 ) {
5332 continue;
5333 }
5334
5335 // Do not modify previously set terms.
5336 if ( ! empty( get_the_terms( $post, $taxonomy ) ) ) {
5337 continue;
5338 }
5339
5340 if ( 'category' === $taxonomy ) {
5341 $default_term_id = (int) get_option( 'default_category', 0 );
5342 } else {
5343 $default_term_id = (int) get_option( 'default_term_' . $taxonomy, 0 );
5344 }
5345
5346 if ( ! $default_term_id ) {
5347 continue;
5348 }
5349 wp_set_post_terms( $post->ID, array( $default_term_id ), $taxonomy );
5350 }
5351
5352 $wpdb->update( $wpdb->posts, array( 'post_status' => 'publish' ), array( 'ID' => $post->ID ) );
5353
5354 clean_post_cache( $post->ID );
5355
5356 $old_status = $post->post_status;
5357 $post->post_status = 'publish';
5358 wp_transition_post_status( 'publish', $old_status, $post );
5359
5360 /** This action is documented in wp-includes/post.php */
5361 do_action( "edit_post_{$post->post_type}", $post->ID, $post );
5362
5363 /** This action is documented in wp-includes/post.php */
5364 do_action( 'edit_post', $post->ID, $post );
5365
5366 /** This action is documented in wp-includes/post.php */
5367 do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
5368
5369 /** This action is documented in wp-includes/post.php */
5370 do_action( 'save_post', $post->ID, $post, true );
5371
5372 /** This action is documented in wp-includes/post.php */
5373 do_action( 'wp_insert_post', $post->ID, $post, true );
5374
5375 wp_after_insert_post( $post, true, $post_before );
5376}
5377
5378/**
5379 * Publishes future post and make sure post ID has future post status.
5380 *
5381 * Invoked by cron 'publish_future_post' event. This safeguard prevents cron
5382 * from publishing drafts, etc.
5383 *
5384 * @since 2.5.0
5385 *
5386 * @param int|WP_Post $post Post ID or post object.
5387 */
5388function check_and_publish_future_post( $post ) {
5389 $post = get_post( $post );
5390
5391 if ( ! $post ) {
5392 return;
5393 }
5394
5395 if ( 'future' !== $post->post_status ) {
5396 return;
5397 }
5398
5399 $time = strtotime( $post->post_date_gmt . ' GMT' );
5400
5401 // Uh oh, someone jumped the gun!
5402 if ( $time > time() ) {
5403 wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); // Clear anything else in the system.
5404 wp_schedule_single_event( $time, 'publish_future_post', array( $post->ID ) );
5405 return;
5406 }
5407
5408 // wp_publish_post() returns no meaningful value.
5409 wp_publish_post( $post->ID );
5410}
5411
5412/**
5413 * Uses wp_checkdate to return a valid Gregorian-calendar value for post_date.
5414 * If post_date is not provided, this first checks post_date_gmt if provided,
5415 * then falls back to use the current time.
5416 *
5417 * For back-compat purposes in wp_insert_post, an empty post_date and an invalid
5418 * post_date_gmt will continue to return '1970-01-01 00:00:00' rather than false.
5419 *
5420 * @since 5.7.0
5421 *
5422 * @param string $post_date The date in mysql format (`Y-m-d H:i:s`).
5423 * @param string $post_date_gmt The GMT date in mysql format (`Y-m-d H:i:s`).
5424 * @return string|false A valid Gregorian-calendar date string, or false on failure.
5425 */
5426function wp_resolve_post_date( $post_date = '', $post_date_gmt = '' ) {
5427 // If the date is empty, set the date to now.
5428 if ( empty( $post_date ) || '0000-00-00 00:00:00' === $post_date ) {
5429 if ( empty( $post_date_gmt ) || '0000-00-00 00:00:00' === $post_date_gmt ) {
5430 $post_date = current_time( 'mysql' );
5431 } else {
5432 $post_date = get_date_from_gmt( $post_date_gmt );
5433 }
5434 }
5435
5436 // Validate the date.
5437 preg_match( '/^(\d{4})-(\d{1,2})-(\d{1,2})/', $post_date, $matches );
5438
5439 if ( empty( $matches ) || ! is_array( $matches ) || count( $matches ) < 4 ) {
5440 return false;
5441 }
5442
5443 $valid_date = wp_checkdate( $matches[2], $matches[3], $matches[1], $post_date );
5444
5445 if ( ! $valid_date ) {
5446 return false;
5447 }
5448 return $post_date;
5449}
5450
5451/**
5452 * Computes a unique slug for the post, when given the desired slug and some post details.
5453 *
5454 * @since 2.8.0
5455 *
5456 * @global wpdb $wpdb WordPress database abstraction object.
5457 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
5458 *
5459 * @param string $slug The desired slug (post_name).
5460 * @param int $post_id Post ID.
5461 * @param string $post_status No uniqueness checks are made if the post is still draft or pending.
5462 * @param string $post_type Post type.
5463 * @param int $post_parent Post parent ID.
5464 * @return string Unique slug for the post, based on $post_name (with a -1, -2, etc. suffix)
5465 */
5466function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_parent ) {
5467 if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true )
5468 || ( 'inherit' === $post_status && 'revision' === $post_type ) || 'user_request' === $post_type
5469 ) {
5470 return $slug;
5471 }
5472
5473 /**
5474 * Filters the post slug before it is generated to be unique.
5475 *
5476 * Returning a non-null value will short-circuit the
5477 * unique slug generation, returning the passed value instead.
5478 *
5479 * @since 5.1.0
5480 *
5481 * @param string|null $override_slug Short-circuit return value.
5482 * @param string $slug The desired slug (post_name).
5483 * @param int $post_id Post ID.
5484 * @param string $post_status The post status.
5485 * @param string $post_type Post type.
5486 * @param int $post_parent Post parent ID.
5487 */
5488 $override_slug = apply_filters( 'pre_wp_unique_post_slug', null, $slug, $post_id, $post_status, $post_type, $post_parent );
5489 if ( null !== $override_slug ) {
5490 return $override_slug;
5491 }
5492
5493 global $wpdb, $wp_rewrite;
5494
5495 $original_slug = $slug;
5496
5497 $feeds = $wp_rewrite->feeds;
5498 if ( ! is_array( $feeds ) ) {
5499 $feeds = array();
5500 }
5501
5502 if ( 'attachment' === $post_type ) {
5503 // Attachment slugs must be unique across all types.
5504 $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND ID != %d LIMIT 1";
5505 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_id ) );
5506
5507 /**
5508 * Filters whether the post slug would make a bad attachment slug.
5509 *
5510 * @since 3.1.0
5511 *
5512 * @param bool $bad_slug Whether the slug would be bad as an attachment slug.
5513 * @param string $slug The post slug.
5514 */
5515 $is_bad_attachment_slug = apply_filters( 'wp_unique_post_slug_is_bad_attachment_slug', false, $slug );
5516
5517 if ( $post_name_check
5518 || in_array( $slug, $feeds, true ) || 'embed' === $slug
5519 || $is_bad_attachment_slug
5520 ) {
5521 $suffix = 2;
5522 do {
5523 $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
5524 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_id ) );
5525 ++$suffix;
5526 } while ( $post_name_check );
5527 $slug = $alt_post_name;
5528 }
5529 } elseif ( is_post_type_hierarchical( $post_type ) ) {
5530 if ( 'nav_menu_item' === $post_type ) {
5531 return $slug;
5532 }
5533
5534 /*
5535 * Page slugs must be unique within their own trees. Pages are in a separate
5536 * namespace than posts so page slugs are allowed to overlap post slugs.
5537 */
5538 $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1";
5539 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id, $post_parent ) );
5540
5541 /**
5542 * Filters whether the post slug would make a bad hierarchical post slug.
5543 *
5544 * @since 3.1.0
5545 *
5546 * @param bool $bad_slug Whether the post slug would be bad in a hierarchical post context.
5547 * @param string $slug The post slug.
5548 * @param string $post_type Post type.
5549 * @param int $post_parent Post parent ID.
5550 */
5551 $is_bad_hierarchical_slug = apply_filters( 'wp_unique_post_slug_is_bad_hierarchical_slug', false, $slug, $post_type, $post_parent );
5552
5553 if ( $post_name_check
5554 || in_array( $slug, $feeds, true ) || 'embed' === $slug
5555 || preg_match( "@^($wp_rewrite->pagination_base)?\d+$@", $slug )
5556 || $is_bad_hierarchical_slug
5557 ) {
5558 $suffix = 2;
5559 do {
5560 $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
5561 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id, $post_parent ) );
5562 ++$suffix;
5563 } while ( $post_name_check );
5564 $slug = $alt_post_name;
5565 }
5566 } else {
5567 // Post slugs must be unique across all posts.
5568 $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1";
5569 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id ) );
5570
5571 $post = get_post( $post_id );
5572
5573 // Prevent new post slugs that could result in URLs that conflict with date archives.
5574 $conflicts_with_date_archive = false;
5575 if ( 'post' === $post_type && ( ! $post || $post->post_name !== $slug ) && preg_match( '/^[0-9]+$/', $slug ) ) {
5576 $slug_num = (int) $slug;
5577
5578 if ( $slug_num ) {
5579 $permastructs = array_values( array_filter( explode( '/', get_option( 'permalink_structure' ) ) ) );
5580 $postname_index = array_search( '%postname%', $permastructs, true );
5581
5582 /*
5583 * Potential date clashes are as follows:
5584 *
5585 * - Any integer in the first permastruct position could be a year.
5586 * - An integer between 1 and 12 that follows 'year' conflicts with 'monthnum'.
5587 * - An integer between 1 and 31 that follows 'monthnum' conflicts with 'day'.
5588 */
5589 if ( 0 === $postname_index ||
5590 ( $postname_index && '%year%' === $permastructs[ $postname_index - 1 ] && 13 > $slug_num ) ||
5591 ( $postname_index && '%monthnum%' === $permastructs[ $postname_index - 1 ] && 32 > $slug_num )
5592 ) {
5593 $conflicts_with_date_archive = true;
5594 }
5595 }
5596 }
5597
5598 /**
5599 * Filters whether the post slug would be bad as a flat slug.
5600 *
5601 * @since 3.1.0
5602 *
5603 * @param bool $bad_slug Whether the post slug would be bad as a flat slug.
5604 * @param string $slug The post slug.
5605 * @param string $post_type Post type.
5606 */
5607 $is_bad_flat_slug = apply_filters( 'wp_unique_post_slug_is_bad_flat_slug', false, $slug, $post_type );
5608
5609 if ( $post_name_check
5610 || in_array( $slug, $feeds, true ) || 'embed' === $slug
5611 || $conflicts_with_date_archive
5612 || $is_bad_flat_slug
5613 ) {
5614 $suffix = 2;
5615 do {
5616 $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
5617 $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id ) );
5618 ++$suffix;
5619 } while ( $post_name_check );
5620 $slug = $alt_post_name;
5621 }
5622 }
5623
5624 /**
5625 * Filters the unique post slug.
5626 *
5627 * @since 3.3.0
5628 *
5629 * @param string $slug The post slug.
5630 * @param int $post_id Post ID.
5631 * @param string $post_status The post status.
5632 * @param string $post_type Post type.
5633 * @param int $post_parent Post parent ID
5634 * @param string $original_slug The original post slug.
5635 */
5636 return apply_filters( 'wp_unique_post_slug', $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug );
5637}
5638
5639/**
5640 * Truncates a post slug.
5641 *
5642 * @since 3.6.0
5643 * @access private
5644 *
5645 * @see utf8_uri_encode()
5646 *
5647 * @param string $slug The slug to truncate.
5648 * @param int $length Optional. Max length of the slug. Default 200 (characters).
5649 * @return string The truncated slug.
5650 */
5651function _truncate_post_slug( $slug, $length = 200 ) {
5652 if ( strlen( $slug ) > $length ) {
5653 $decoded_slug = urldecode( $slug );
5654 if ( $decoded_slug === $slug ) {
5655 $slug = substr( $slug, 0, $length );
5656 } else {
5657 $slug = utf8_uri_encode( $decoded_slug, $length, true );
5658 }
5659 }
5660
5661 return rtrim( $slug, '-' );
5662}
5663
5664/**
5665 * Adds tags to a post.
5666 *
5667 * @see wp_set_post_tags()
5668 *
5669 * @since 2.3.0
5670 *
5671 * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post.
5672 * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags
5673 * separated by commas. Default empty.
5674 * @return array|false|WP_Error Array of affected term IDs. WP_Error or false on failure.
5675 */
5676function wp_add_post_tags( $post_id = 0, $tags = '' ) {
5677 return wp_set_post_tags( $post_id, $tags, true );
5678}
5679
5680/**
5681 * Sets the tags for a post.
5682 *
5683 * @since 2.3.0
5684 *
5685 * @see wp_set_object_terms()
5686 *
5687 * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post.
5688 * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags
5689 * separated by commas. Default empty.
5690 * @param bool $append Optional. If true, don't delete existing tags, just add on. If false,
5691 * replace the tags with the new tags. Default false.
5692 * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure.
5693 */
5694function wp_set_post_tags( $post_id = 0, $tags = '', $append = false ) {
5695 return wp_set_post_terms( $post_id, $tags, 'post_tag', $append );
5696}
5697
5698/**
5699 * Sets the terms for a post.
5700 *
5701 * @since 2.8.0
5702 *
5703 * @see wp_set_object_terms()
5704 *
5705 * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post.
5706 * @param string|array $terms Optional. An array of terms to set for the post, or a string of terms
5707 * separated by commas. Hierarchical taxonomies must always pass IDs rather
5708 * than names so that children with the same names but different parents
5709 * aren't confused. Default empty.
5710 * @param string $taxonomy Optional. Taxonomy name. Default 'post_tag'.
5711 * @param bool $append Optional. If true, don't delete existing terms, just add on. If false,
5712 * replace the terms with the new terms. Default false.
5713 * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure.
5714 */
5715function wp_set_post_terms( $post_id = 0, $terms = '', $taxonomy = 'post_tag', $append = false ) {
5716 $post_id = (int) $post_id;
5717
5718 if ( ! $post_id ) {
5719 return false;
5720 }
5721
5722 if ( empty( $terms ) ) {
5723 $terms = array();
5724 }
5725
5726 if ( ! is_array( $terms ) ) {
5727 $comma = _x( ',', 'tag delimiter' );
5728 if ( ',' !== $comma ) {
5729 $terms = str_replace( $comma, ',', $terms );
5730 }
5731 $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) );
5732 }
5733
5734 /*
5735 * Hierarchical taxonomies must always pass IDs rather than names so that
5736 * children with the same names but different parents aren't confused.
5737 */
5738 if ( is_taxonomy_hierarchical( $taxonomy ) ) {
5739 $terms = array_unique( array_map( 'intval', $terms ) );
5740 }
5741
5742 return wp_set_object_terms( $post_id, $terms, $taxonomy, $append );
5743}
5744
5745/**
5746 * Sets categories for a post.
5747 *
5748 * If no categories are provided, the default category is used.
5749 *
5750 * @since 2.1.0
5751 *
5752 * @param int $post_id Optional. The Post ID. Does not default to the ID
5753 * of the global $post. Default 0.
5754 * @param int[]|int $post_categories Optional. List of category IDs, or the ID of a single category.
5755 * Default empty array.
5756 * @param bool $append If true, don't delete existing categories, just add on.
5757 * If false, replace the categories with the new categories.
5758 * @return array|false|WP_Error Array of term taxonomy IDs of affected categories. WP_Error or false on failure.
5759 */
5760function wp_set_post_categories( $post_id = 0, $post_categories = array(), $append = false ) {
5761 $post_id = (int) $post_id;
5762 $post_type = get_post_type( $post_id );
5763 $post_status = get_post_status( $post_id );
5764
5765 // If $post_categories isn't already an array, make it one.
5766 $post_categories = (array) $post_categories;
5767
5768 if ( empty( $post_categories ) ) {
5769 /**
5770 * Filters post types (in addition to 'post') that require a default category.
5771 *
5772 * @since 5.5.0
5773 *
5774 * @param string[] $post_types An array of post type names. Default empty array.
5775 */
5776 $default_category_post_types = apply_filters( 'default_category_post_types', array() );
5777
5778 // Regular posts always require a default category.
5779 $default_category_post_types = array_merge( $default_category_post_types, array( 'post' ) );
5780
5781 if ( in_array( $post_type, $default_category_post_types, true )
5782 && is_object_in_taxonomy( $post_type, 'category' )
5783 && 'auto-draft' !== $post_status
5784 ) {
5785 $post_categories = array( get_option( 'default_category' ) );
5786 $append = false;
5787 } else {
5788 $post_categories = array();
5789 }
5790 } elseif ( 1 === count( $post_categories ) && '' === reset( $post_categories ) ) {
5791 return true;
5792 }
5793
5794 return wp_set_post_terms( $post_id, $post_categories, 'category', $append );
5795}
5796
5797/**
5798 * Fires actions related to the transitioning of a post's status.
5799 *
5800 * When a post is saved, the post status is "transitioned" from one status to another,
5801 * though this does not always mean the status has actually changed before and after
5802 * the save. This function fires a number of action hooks related to that transition:
5803 * the generic {@see 'transition_post_status'} action, as well as the dynamic hooks
5804 * {@see '$old_status_to_$new_status'} and {@see '$new_status_$post->post_type'}. Note
5805 * that the function does not transition the post object in the database.
5806 *
5807 * For instance: When publishing a post for the first time, the post status may transition
5808 * from 'draft' – or some other status – to 'publish'. However, if a post is already
5809 * published and is simply being updated, the "old" and "new" statuses may both be 'publish'
5810 * before and after the transition.
5811 *
5812 * @since 2.3.0
5813 *
5814 * @param string $new_status Transition to this post status.
5815 * @param string $old_status Previous post status.
5816 * @param WP_Post $post Post data.
5817 */
5818function wp_transition_post_status( $new_status, $old_status, $post ) {
5819 /**
5820 * Fires when a post is transitioned from one status to another.
5821 *
5822 * @since 2.3.0
5823 *
5824 * @param string $new_status New post status.
5825 * @param string $old_status Old post status.
5826 * @param WP_Post $post Post object.
5827 */
5828 do_action( 'transition_post_status', $new_status, $old_status, $post );
5829
5830 /**
5831 * Fires when a post is transitioned from one status to another.
5832 *
5833 * The dynamic portions of the hook name, `$new_status` and `$old_status`,
5834 * refer to the old and new post statuses, respectively.
5835 *
5836 * Possible hook names include:
5837 *
5838 * - `draft_to_publish`
5839 * - `publish_to_trash`
5840 * - `pending_to_draft`
5841 *
5842 * @since 2.3.0
5843 *
5844 * @param WP_Post $post Post object.
5845 */
5846 do_action( "{$old_status}_to_{$new_status}", $post );
5847
5848 /**
5849 * Fires when a post is transitioned from one status to another.
5850 *
5851 * The dynamic portions of the hook name, `$new_status` and `$post->post_type`,
5852 * refer to the new post status and post type, respectively.
5853 *
5854 * Possible hook names include:
5855 *
5856 * - `draft_post`
5857 * - `future_post`
5858 * - `pending_post`
5859 * - `private_post`
5860 * - `publish_post`
5861 * - `trash_post`
5862 * - `draft_page`
5863 * - `future_page`
5864 * - `pending_page`
5865 * - `private_page`
5866 * - `publish_page`
5867 * - `trash_page`
5868 * - `publish_attachment`
5869 * - `trash_attachment`
5870 *
5871 * Please note: When this action is hooked using a particular post status (like
5872 * 'publish', as `publish_{$post->post_type}`), it will fire both when a post is
5873 * first transitioned to that status from something else, as well as upon
5874 * subsequent post updates (old and new status are both the same).
5875 *
5876 * Therefore, if you are looking to only fire a callback when a post is first
5877 * transitioned to a status, use the {@see 'transition_post_status'} hook instead.
5878 *
5879 * @since 2.3.0
5880 * @since 5.9.0 Added `$old_status` parameter.
5881 *
5882 * @param int $post_id Post ID.
5883 * @param WP_Post $post Post object.
5884 * @param string $old_status Old post status.
5885 */
5886 do_action( "{$new_status}_{$post->post_type}", $post->ID, $post, $old_status );
5887}
5888
5889/**
5890 * Fires actions after a post, its terms and meta data has been saved.
5891 *
5892 * @since 5.6.0
5893 *
5894 * @param int|WP_Post $post The post ID or object that has been saved.
5895 * @param bool $update Whether this is an existing post being updated.
5896 * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
5897 * to the update for updated posts.
5898 */
5899function wp_after_insert_post( $post, $update, $post_before ) {
5900 $post = get_post( $post );
5901
5902 if ( ! $post ) {
5903 return;
5904 }
5905
5906 $post_id = $post->ID;
5907
5908 /**
5909 * Fires once a post, its terms and meta data has been saved.
5910 *
5911 * @since 5.6.0
5912 *
5913 * @param int $post_id Post ID.
5914 * @param WP_Post $post Post object.
5915 * @param bool $update Whether this is an existing post being updated.
5916 * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
5917 * to the update for updated posts.
5918 */
5919 do_action( 'wp_after_insert_post', $post_id, $post, $update, $post_before );
5920}
5921
5922//
5923// Comment, trackback, and pingback functions.
5924//
5925
5926/**
5927 * Adds a URL to those already pinged.
5928 *
5929 * @since 1.5.0
5930 * @since 4.7.0 `$post` can be a WP_Post object.
5931 * @since 4.7.0 `$uri` can be an array of URIs.
5932 *
5933 * @global wpdb $wpdb WordPress database abstraction object.
5934 *
5935 * @param int|WP_Post $post Post ID or post object.
5936 * @param string|array $uri Ping URI or array of URIs.
5937 * @return int|false How many rows were updated.
5938 */
5939function add_ping( $post, $uri ) {
5940 global $wpdb;
5941
5942 $post = get_post( $post );
5943
5944 if ( ! $post ) {
5945 return false;
5946 }
5947
5948 $pung = trim( $post->pinged );
5949 $pung = preg_split( '/\s/', $pung );
5950
5951 if ( is_array( $uri ) ) {
5952 $pung = array_merge( $pung, $uri );
5953 } else {
5954 $pung[] = $uri;
5955 }
5956 $new = implode( "\n", $pung );
5957
5958 /**
5959 * Filters the new ping URL to add for the given post.
5960 *
5961 * @since 2.0.0
5962 *
5963 * @param string $new New ping URL to add.
5964 */
5965 $new = apply_filters( 'add_ping', $new );
5966
5967 $return = $wpdb->update( $wpdb->posts, array( 'pinged' => $new ), array( 'ID' => $post->ID ) );
5968 clean_post_cache( $post->ID );
5969 return $return;
5970}
5971
5972/**
5973 * Retrieves enclosures already enclosed for a post.
5974 *
5975 * @since 1.5.0
5976 *
5977 * @param int $post_id Post ID.
5978 * @return string[] Array of enclosures for the given post.
5979 */
5980function get_enclosed( $post_id ) {
5981 $custom_fields = get_post_custom( $post_id );
5982 $pung = array();
5983 if ( ! is_array( $custom_fields ) ) {
5984 return $pung;
5985 }
5986
5987 foreach ( $custom_fields as $key => $val ) {
5988 if ( 'enclosure' !== $key || ! is_array( $val ) ) {
5989 continue;
5990 }
5991 foreach ( $val as $enc ) {
5992 $enclosure = explode( "\n", $enc );
5993 $pung[] = trim( $enclosure[0] );
5994 }
5995 }
5996
5997 /**
5998 * Filters the list of enclosures already enclosed for the given post.
5999 *
6000 * @since 2.0.0
6001 *
6002 * @param string[] $pung Array of enclosures for the given post.
6003 * @param int $post_id Post ID.
6004 */
6005 return apply_filters( 'get_enclosed', $pung, $post_id );
6006}
6007
6008/**
6009 * Retrieves URLs already pinged for a post.
6010 *
6011 * @since 1.5.0
6012 *
6013 * @since 4.7.0 `$post` can be a WP_Post object.
6014 *
6015 * @param int|WP_Post $post Post ID or object.
6016 * @return string[]|false Array of URLs already pinged for the given post, false if the post is not found.
6017 */
6018function get_pung( $post ) {
6019 $post = get_post( $post );
6020
6021 if ( ! $post ) {
6022 return false;
6023 }
6024
6025 $pung = trim( $post->pinged );
6026 $pung = preg_split( '/\s/', $pung );
6027
6028 /**
6029 * Filters the list of already-pinged URLs for the given post.
6030 *
6031 * @since 2.0.0
6032 *
6033 * @param string[] $pung Array of URLs already pinged for the given post.
6034 */
6035 return apply_filters( 'get_pung', $pung );
6036}
6037
6038/**
6039 * Retrieves URLs that need to be pinged.
6040 *
6041 * @since 1.5.0
6042 * @since 4.7.0 `$post` can be a WP_Post object.
6043 *
6044 * @param int|WP_Post $post Post ID or post object.
6045 * @return string[]|false List of URLs yet to ping.
6046 */
6047function get_to_ping( $post ) {
6048 $post = get_post( $post );
6049
6050 if ( ! $post ) {
6051 return false;
6052 }
6053
6054 $to_ping = sanitize_trackback_urls( $post->to_ping );
6055 $to_ping = preg_split( '/\s/', $to_ping, -1, PREG_SPLIT_NO_EMPTY );
6056
6057 /**
6058 * Filters the list of URLs yet to ping for the given post.
6059 *
6060 * @since 2.0.0
6061 *
6062 * @param string[] $to_ping List of URLs yet to ping.
6063 */
6064 return apply_filters( 'get_to_ping', $to_ping );
6065}
6066
6067/**
6068 * Does trackbacks for a list of URLs.
6069 *
6070 * @since 1.0.0
6071 *
6072 * @param string $tb_list Comma separated list of URLs.
6073 * @param int $post_id Post ID.
6074 */
6075function trackback_url_list( $tb_list, $post_id ) {
6076 if ( ! empty( $tb_list ) ) {
6077 // Get post data.
6078 $postdata = get_post( $post_id, ARRAY_A );
6079
6080 // Form an excerpt.
6081 $excerpt = strip_tags( $postdata['post_excerpt'] ? $postdata['post_excerpt'] : $postdata['post_content'] );
6082
6083 if ( strlen( $excerpt ) > 255 ) {
6084 $excerpt = substr( $excerpt, 0, 252 ) . '…';
6085 }
6086
6087 $trackback_urls = explode( ',', $tb_list );
6088 foreach ( (array) $trackback_urls as $tb_url ) {
6089 $tb_url = trim( $tb_url );
6090 trackback( $tb_url, wp_unslash( $postdata['post_title'] ), $excerpt, $post_id );
6091 }
6092 }
6093}
6094
6095//
6096// Page functions.
6097//
6098
6099/**
6100 * Gets a list of page IDs.
6101 *
6102 * @since 2.0.0
6103 *
6104 * @global wpdb $wpdb WordPress database abstraction object.
6105 *
6106 * @return string[] List of page IDs as strings.
6107 */
6108function get_all_page_ids() {
6109 global $wpdb;
6110
6111 $page_ids = wp_cache_get( 'all_page_ids', 'posts' );
6112 if ( ! is_array( $page_ids ) ) {
6113 $page_ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type = 'page'" );
6114 wp_cache_add( 'all_page_ids', $page_ids, 'posts' );
6115 }
6116
6117 return $page_ids;
6118}
6119
6120/**
6121 * Retrieves page data given a page ID or page object.
6122 *
6123 * Use get_post() instead of get_page().
6124 *
6125 * @since 1.5.1
6126 * @deprecated 3.5.0 Use get_post()
6127 *
6128 * @param int|WP_Post $page Page object or page ID. Passed by reference.
6129 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
6130 * correspond to a WP_Post object, an associative array, or a numeric array,
6131 * respectively. Default OBJECT.
6132 * @param string $filter Optional. How the return value should be filtered. Accepts 'raw',
6133 * 'edit', 'db', 'display'. Default 'raw'.
6134 * @return WP_Post|array|null WP_Post or array on success, null on failure.
6135 */
6136function get_page( $page, $output = OBJECT, $filter = 'raw' ) {
6137 return get_post( $page, $output, $filter );
6138}
6139
6140/**
6141 * Retrieves a page given its path.
6142 *
6143 * @since 2.1.0
6144 *
6145 * @global wpdb $wpdb WordPress database abstraction object.
6146 *
6147 * @param string $page_path Page path.
6148 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
6149 * correspond to a WP_Post object, an associative array, or a numeric array,
6150 * respectively. Default OBJECT.
6151 * @param string|array $post_type Optional. Post type or array of post types. Default 'page'.
6152 * @return WP_Post|array|null WP_Post (or array) on success, or null on failure.
6153 */
6154function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
6155 global $wpdb;
6156
6157 $last_changed = wp_cache_get_last_changed( 'posts' );
6158
6159 $hash = md5( $page_path . serialize( $post_type ) );
6160 $cache_key = "get_page_by_path:$hash";
6161 $cached = wp_cache_get_salted( $cache_key, 'post-queries', $last_changed );
6162 if ( false !== $cached ) {
6163 // Special case: '0' is a bad `$page_path`.
6164 if ( '0' === $cached || 0 === $cached ) {
6165 return null;
6166 } else {
6167 return get_post( $cached, $output );
6168 }
6169 }
6170
6171 $page_path = rawurlencode( urldecode( $page_path ) );
6172 $page_path = str_replace( '%2F', '/', $page_path );
6173 $page_path = str_replace( '%20', ' ', $page_path );
6174 $parts = explode( '/', trim( $page_path, '/' ) );
6175 $parts = array_map( 'sanitize_title_for_query', $parts );
6176 $escaped_parts = esc_sql( $parts );
6177
6178 $in_string = "'" . implode( "','", $escaped_parts ) . "'";
6179
6180 if ( is_array( $post_type ) ) {
6181 $post_types = $post_type;
6182 } else {
6183 $post_types = array( $post_type, 'attachment' );
6184 }
6185
6186 $post_types = esc_sql( $post_types );
6187 $post_type_in_string = "'" . implode( "','", $post_types ) . "'";
6188 $sql = "
6189 SELECT ID, post_name, post_parent, post_type
6190 FROM $wpdb->posts
6191 WHERE post_name IN ($in_string)
6192 AND post_type IN ($post_type_in_string)
6193 ";
6194
6195 $pages = $wpdb->get_results( $sql, OBJECT_K );
6196
6197 $revparts = array_reverse( $parts );
6198
6199 $found_id = 0;
6200 foreach ( (array) $pages as $page ) {
6201 if ( $page->post_name === $revparts[0] ) {
6202 $count = 0;
6203 $p = $page;
6204
6205 /*
6206 * Loop through the given path parts from right to left,
6207 * ensuring each matches the post ancestry.
6208 */
6209 while ( 0 !== (int) $p->post_parent && isset( $pages[ $p->post_parent ] ) ) {
6210 ++$count;
6211 $parent = $pages[ $p->post_parent ];
6212 if ( ! isset( $revparts[ $count ] ) || $parent->post_name !== $revparts[ $count ] ) {
6213 break;
6214 }
6215 $p = $parent;
6216 }
6217
6218 if ( 0 === (int) $p->post_parent
6219 && count( $revparts ) === $count + 1
6220 && $p->post_name === $revparts[ $count ]
6221 ) {
6222 $found_id = $page->ID;
6223 if ( $page->post_type === $post_type ) {
6224 break;
6225 }
6226 }
6227 }
6228 }
6229
6230 // We cache misses as well as hits.
6231 wp_cache_set_salted( $cache_key, $found_id, 'post-queries', $last_changed );
6232
6233 if ( $found_id ) {
6234 return get_post( $found_id, $output );
6235 }
6236
6237 return null;
6238}
6239
6240/**
6241 * Identifies descendants of a given page ID in a list of page objects.
6242 *
6243 * Descendants are identified from the `$pages` array passed to the function. No database queries are performed.
6244 *
6245 * @since 1.5.1
6246 *
6247 * @param int $page_id Page ID.
6248 * @param WP_Post[] $pages List of page objects from which descendants should be identified.
6249 * @return WP_Post[] List of page children.
6250 */
6251function get_page_children( $page_id, $pages ) {
6252 // Build a hash of ID -> children.
6253 $children = array();
6254 foreach ( (array) $pages as $page ) {
6255 $children[ (int) $page->post_parent ][] = $page;
6256 }
6257
6258 $page_list = array();
6259
6260 // Start the search by looking at immediate children.
6261 if ( isset( $children[ $page_id ] ) ) {
6262 // Always start at the end of the stack in order to preserve original `$pages` order.
6263 $to_look = array_reverse( $children[ $page_id ] );
6264
6265 while ( $to_look ) {
6266 $p = array_pop( $to_look );
6267 $page_list[] = $p;
6268 if ( isset( $children[ $p->ID ] ) ) {
6269 foreach ( array_reverse( $children[ $p->ID ] ) as $child ) {
6270 // Append to the `$to_look` stack to descend the tree.
6271 $to_look[] = $child;
6272 }
6273 }
6274 }
6275 }
6276
6277 return $page_list;
6278}
6279
6280/**
6281 * Orders the pages with children under parents in a flat list.
6282 *
6283 * It uses auxiliary structure to hold parent-children relationships and
6284 * runs in O(N) complexity
6285 *
6286 * @since 2.0.0
6287 *
6288 * @param WP_Post[] $pages Posts array (passed by reference).
6289 * @param int $page_id Optional. Parent page ID. Default 0.
6290 * @return string[] Array of post names keyed by ID and arranged by hierarchy. Children immediately follow their parents.
6291 */
6292function get_page_hierarchy( &$pages, $page_id = 0 ) {
6293 if ( empty( $pages ) ) {
6294 return array();
6295 }
6296
6297 $children = array();
6298 foreach ( (array) $pages as $p ) {
6299 $parent_id = (int) $p->post_parent;
6300 $children[ $parent_id ][] = $p;
6301 }
6302
6303 $result = array();
6304 _page_traverse_name( $page_id, $children, $result );
6305
6306 return $result;
6307}
6308
6309/**
6310 * Traverses and return all the nested children post names of a root page.
6311 *
6312 * $children contains parent-children relations
6313 *
6314 * @since 2.9.0
6315 * @access private
6316 *
6317 * @see _page_traverse_name()
6318 *
6319 * @param int $page_id Page ID.
6320 * @param array $children Parent-children relations (passed by reference).
6321 * @param string[] $result Array of page names keyed by ID (passed by reference).
6322 */
6323function _page_traverse_name( $page_id, &$children, &$result ) {
6324 if ( isset( $children[ $page_id ] ) ) {
6325 foreach ( (array) $children[ $page_id ] as $child ) {
6326 $result[ $child->ID ] = $child->post_name;
6327 _page_traverse_name( $child->ID, $children, $result );
6328 }
6329 }
6330}
6331
6332/**
6333 * Builds the URI path for a page.
6334 *
6335 * Sub pages will be in the "directory" under the parent page post name.
6336 *
6337 * @since 1.5.0
6338 * @since 4.6.0 The `$page` parameter was made optional.
6339 *
6340 * @param WP_Post|object|int $page Optional. Page ID or WP_Post object. Default is global $post.
6341 * @return string|false Page URI, false on error.
6342 */
6343function get_page_uri( $page = 0 ) {
6344 if ( ! $page instanceof WP_Post ) {
6345 $page = get_post( $page );
6346 }
6347
6348 if ( ! $page ) {
6349 return false;
6350 }
6351
6352 $uri = $page->post_name;
6353
6354 foreach ( $page->ancestors as $parent ) {
6355 $parent = get_post( $parent );
6356 if ( $parent && $parent->post_name ) {
6357 $uri = $parent->post_name . '/' . $uri;
6358 }
6359 }
6360
6361 /**
6362 * Filters the URI for a page.
6363 *
6364 * @since 4.4.0
6365 *
6366 * @param string $uri Page URI.
6367 * @param WP_Post $page Page object.
6368 */
6369 return apply_filters( 'get_page_uri', $uri, $page );
6370}
6371
6372/**
6373 * Retrieves an array of pages (or hierarchical post type items).
6374 *
6375 * @since 1.5.0
6376 * @since 6.3.0 Use WP_Query internally.
6377 *
6378 * @param array|string $args {
6379 * Optional. Array or string of arguments to retrieve pages.
6380 *
6381 * @type int $child_of Page ID to return child and grandchild pages of. Note: The value
6382 * of `$hierarchical` has no bearing on whether `$child_of` returns
6383 * hierarchical results. Default 0, or no restriction.
6384 * @type string $sort_order How to sort retrieved pages. Accepts 'ASC', 'DESC'. Default 'ASC'.
6385 * @type string $sort_column What columns to sort pages by, comma-separated. Accepts 'post_author',
6386 * 'post_date', 'post_title', 'post_name', 'post_modified', 'menu_order',
6387 * 'post_modified_gmt', 'post_parent', 'ID', 'rand', 'comment_count'.
6388 * 'post_' can be omitted for any values that start with it.
6389 * Default 'post_title'.
6390 * @type bool $hierarchical Whether to return pages hierarchically. If false in conjunction with
6391 * `$child_of` also being false, both arguments will be disregarded.
6392 * Default true.
6393 * @type int[] $exclude Array of page IDs to exclude. Default empty array.
6394 * @type int[] $include Array of page IDs to include. Cannot be used with `$child_of`,
6395 * `$parent`, `$exclude`, `$meta_key`, `$meta_value`, or `$hierarchical`.
6396 * Default empty array.
6397 * @type string $meta_key Only include pages with this meta key. Default empty.
6398 * @type string $meta_value Only include pages with this meta value. Requires `$meta_key`.
6399 * Default empty.
6400 * @type string $authors A comma-separated list of author IDs. Default empty.
6401 * @type int $parent Page ID to return direct children of. Default -1, or no restriction.
6402 * @type string|int[] $exclude_tree Comma-separated string or array of page IDs to exclude.
6403 * Default empty array.
6404 * @type int $number The number of pages to return. Default 0, or all pages.
6405 * @type int $offset The number of pages to skip before returning. Requires `$number`.
6406 * Default 0.
6407 * @type string $post_type The post type to query. Default 'page'.
6408 * @type string|array $post_status A comma-separated list or array of post statuses to include.
6409 * Default 'publish'.
6410 * }
6411 * @return WP_Post[]|false Array of pages (or hierarchical post type items). Boolean false if the
6412 * specified post type is not hierarchical or the specified status is not
6413 * supported by the post type.
6414 */
6415function get_pages( $args = array() ) {
6416 $defaults = array(
6417 'child_of' => 0,
6418 'sort_order' => 'ASC',
6419 'sort_column' => 'post_title',
6420 'hierarchical' => 1,
6421 'exclude' => array(),
6422 'include' => array(),
6423 'meta_key' => '',
6424 'meta_value' => '',
6425 'authors' => '',
6426 'parent' => -1,
6427 'exclude_tree' => array(),
6428 'number' => '',
6429 'offset' => 0,
6430 'post_type' => 'page',
6431 'post_status' => 'publish',
6432 );
6433
6434 $parsed_args = wp_parse_args( $args, $defaults );
6435
6436 $number = (int) $parsed_args['number'];
6437 $offset = (int) $parsed_args['offset'];
6438 $child_of = (int) $parsed_args['child_of'];
6439 $hierarchical = $parsed_args['hierarchical'];
6440 $exclude = $parsed_args['exclude'];
6441 $meta_key = $parsed_args['meta_key'];
6442 $meta_value = $parsed_args['meta_value'];
6443 $parent = $parsed_args['parent'];
6444 $post_status = $parsed_args['post_status'];
6445
6446 // Make sure the post type is hierarchical.
6447 $hierarchical_post_types = get_post_types( array( 'hierarchical' => true ) );
6448 if ( ! in_array( $parsed_args['post_type'], $hierarchical_post_types, true ) ) {
6449 return false;
6450 }
6451
6452 if ( $parent > 0 && ! $child_of ) {
6453 $hierarchical = false;
6454 }
6455
6456 // Make sure we have a valid post status.
6457 if ( ! is_array( $post_status ) ) {
6458 $post_status = explode( ',', $post_status );
6459 }
6460 if ( array_diff( $post_status, get_post_stati() ) ) {
6461 return false;
6462 }
6463
6464 $query_args = array(
6465 'orderby' => 'post_title',
6466 'order' => 'ASC',
6467 'post__not_in' => wp_parse_id_list( $exclude ),
6468 'meta_key' => $meta_key,
6469 'meta_value' => $meta_value,
6470 'posts_per_page' => -1,
6471 'offset' => $offset,
6472 'post_type' => $parsed_args['post_type'],
6473 'post_status' => $post_status,
6474 'update_post_term_cache' => false,
6475 'update_post_meta_cache' => false,
6476 'ignore_sticky_posts' => true,
6477 'no_found_rows' => true,
6478 );
6479
6480 if ( ! empty( $parsed_args['include'] ) ) {
6481 $child_of = 0; // Ignore child_of, parent, exclude, meta_key, and meta_value params if using include.
6482 $parent = -1;
6483 unset( $query_args['post__not_in'], $query_args['meta_key'], $query_args['meta_value'] );
6484 $hierarchical = false;
6485 $query_args['post__in'] = wp_parse_id_list( $parsed_args['include'] );
6486 }
6487
6488 if ( ! empty( $parsed_args['authors'] ) ) {
6489 $post_authors = wp_parse_list( $parsed_args['authors'] );
6490
6491 if ( ! empty( $post_authors ) ) {
6492 $query_args['author__in'] = array();
6493 foreach ( $post_authors as $post_author ) {
6494 // Do we have an author id or an author login?
6495 if ( 0 === (int) $post_author ) {
6496 $post_author = get_user_by( 'login', $post_author );
6497 if ( empty( $post_author ) ) {
6498 continue;
6499 }
6500 if ( empty( $post_author->ID ) ) {
6501 continue;
6502 }
6503 $post_author = $post_author->ID;
6504 }
6505 $query_args['author__in'][] = (int) $post_author;
6506 }
6507 }
6508 }
6509
6510 if ( is_array( $parent ) ) {
6511 $post_parent__in = array_map( 'absint', (array) $parent );
6512 if ( ! empty( $post_parent__in ) ) {
6513 $query_args['post_parent__in'] = $post_parent__in;
6514 }
6515 } elseif ( $parent >= 0 ) {
6516 $query_args['post_parent'] = $parent;
6517 }
6518
6519 /*
6520 * Maintain backward compatibility for `sort_column` key.
6521 * Additionally to `WP_Query`, it has been supporting the `post_modified_gmt` field, so this logic will translate
6522 * it to `post_modified` which should result in the same order given the two dates in the fields match.
6523 */
6524 $orderby = wp_parse_list( $parsed_args['sort_column'] );
6525 $orderby = array_map(
6526 static function ( $orderby_field ) {
6527 $orderby_field = trim( $orderby_field );
6528 if ( 'post_modified_gmt' === $orderby_field || 'modified_gmt' === $orderby_field ) {
6529 $orderby_field = str_replace( '_gmt', '', $orderby_field );
6530 }
6531 return $orderby_field;
6532 },
6533 $orderby
6534 );
6535 if ( $orderby ) {
6536 $query_args['orderby'] = array_fill_keys( $orderby, $parsed_args['sort_order'] );
6537 }
6538
6539 $order = $parsed_args['sort_order'];
6540 if ( $order ) {
6541 $query_args['order'] = $order;
6542 }
6543
6544 if ( ! empty( $number ) ) {
6545 $query_args['posts_per_page'] = $number;
6546 }
6547
6548 /**
6549 * Filters query arguments passed to WP_Query in get_pages.
6550 *
6551 * @since 6.3.0
6552 *
6553 * @param array $query_args Array of arguments passed to WP_Query.
6554 * @param array $parsed_args Array of get_pages() arguments.
6555 */
6556 $query_args = apply_filters( 'get_pages_query_args', $query_args, $parsed_args );
6557
6558 $pages = new WP_Query();
6559 $pages = $pages->query( $query_args );
6560
6561 if ( $child_of || $hierarchical ) {
6562 $pages = get_page_children( $child_of, $pages );
6563 }
6564
6565 if ( ! empty( $parsed_args['exclude_tree'] ) ) {
6566 $exclude = wp_parse_id_list( $parsed_args['exclude_tree'] );
6567 foreach ( $exclude as $id ) {
6568 $children = get_page_children( $id, $pages );
6569 foreach ( $children as $child ) {
6570 $exclude[] = $child->ID;
6571 }
6572 }
6573
6574 $num_pages = count( $pages );
6575 for ( $i = 0; $i < $num_pages; $i++ ) {
6576 if ( in_array( $pages[ $i ]->ID, $exclude, true ) ) {
6577 unset( $pages[ $i ] );
6578 }
6579 }
6580 }
6581
6582 /**
6583 * Filters the retrieved list of pages.
6584 *
6585 * @since 2.1.0
6586 *
6587 * @param WP_Post[] $pages Array of page objects.
6588 * @param array $parsed_args Array of get_pages() arguments.
6589 */
6590 return apply_filters( 'get_pages', $pages, $parsed_args );
6591}
6592
6593//
6594// Attachment functions.
6595//
6596
6597/**
6598 * Determines whether an attachment URI is local and really an attachment.
6599 *
6600 * For more information on this and similar theme functions, check out
6601 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
6602 * Conditional Tags} article in the Theme Developer Handbook.
6603 *
6604 * @since 2.0.0
6605 *
6606 * @param string $url URL to check
6607 * @return bool True on success, false on failure.
6608 */
6609function is_local_attachment( $url ) {
6610 if ( ! str_contains( $url, home_url() ) ) {
6611 return false;
6612 }
6613 if ( str_contains( $url, home_url( '/?attachment_id=' ) ) ) {
6614 return true;
6615 }
6616
6617 $id = url_to_postid( $url );
6618 if ( $id ) {
6619 $post = get_post( $id );
6620 if ( 'attachment' === $post->post_type ) {
6621 return true;
6622 }
6623 }
6624 return false;
6625}
6626
6627/**
6628 * Inserts an attachment.
6629 *
6630 * If you set the 'ID' in the $args parameter, it will mean that you are
6631 * updating and attempt to update the attachment. You can also set the
6632 * attachment name or title by setting the key 'post_name' or 'post_title'.
6633 *
6634 * You can set the dates for the attachment manually by setting the 'post_date'
6635 * and 'post_date_gmt' keys' values.
6636 *
6637 * By default, the comments will use the default settings for whether the
6638 * comments are allowed. You can close them manually or keep them open by
6639 * setting the value for the 'comment_status' key.
6640 *
6641 * @since 2.0.0
6642 * @since 4.7.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure.
6643 * @since 5.6.0 Added the `$fire_after_hooks` parameter.
6644 *
6645 * @see wp_insert_post()
6646 *
6647 * @param string|array $args Arguments for inserting an attachment.
6648 * @param string|false $file Optional. Filename. Default false.
6649 * @param int $parent_post_id Optional. Parent post ID or 0 for no parent. Default 0.
6650 * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false.
6651 * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true.
6652 * @return int|WP_Error The attachment ID on success. The value 0 or WP_Error on failure.
6653 */
6654function wp_insert_attachment( $args, $file = false, $parent_post_id = 0, $wp_error = false, $fire_after_hooks = true ) {
6655 $defaults = array(
6656 'file' => $file,
6657 'post_parent' => 0,
6658 );
6659
6660 $data = wp_parse_args( $args, $defaults );
6661
6662 if ( ! empty( $parent_post_id ) ) {
6663 $data['post_parent'] = $parent_post_id;
6664 }
6665
6666 $data['post_type'] = 'attachment';
6667
6668 return wp_insert_post( $data, $wp_error, $fire_after_hooks );
6669}
6670
6671/**
6672 * Trashes or deletes an attachment.
6673 *
6674 * When an attachment is permanently deleted, the file will also be removed.
6675 * Deletion removes all post meta fields, taxonomy, comments, etc. associated
6676 * with the attachment (except the main post).
6677 *
6678 * The attachment is moved to the Trash instead of permanently deleted unless Trash
6679 * for media is disabled, item is already in the Trash, or $force_delete is true.
6680 *
6681 * @since 2.0.0
6682 *
6683 * @global wpdb $wpdb WordPress database abstraction object.
6684 *
6685 * @param int $post_id Attachment ID.
6686 * @param bool $force_delete Optional. Whether to bypass Trash and force deletion.
6687 * Default false.
6688 * @return WP_Post|false|null Post data on success, false or null on failure.
6689 */
6690function wp_delete_attachment( $post_id, $force_delete = false ) {
6691 global $wpdb;
6692
6693 $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) );
6694
6695 if ( ! $post ) {
6696 return $post;
6697 }
6698
6699 $post = get_post( $post );
6700
6701 if ( 'attachment' !== $post->post_type ) {
6702 return false;
6703 }
6704
6705 if ( ! $force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' !== $post->post_status ) {
6706 return wp_trash_post( $post_id );
6707 }
6708
6709 /**
6710 * Filters whether an attachment deletion should take place.
6711 *
6712 * @since 5.5.0
6713 *
6714 * @param WP_Post|false|null $delete Whether to go forward with deletion.
6715 * @param WP_Post $post Post object.
6716 * @param bool $force_delete Whether to bypass the Trash.
6717 */
6718 $check = apply_filters( 'pre_delete_attachment', null, $post, $force_delete );
6719 if ( null !== $check ) {
6720 return $check;
6721 }
6722
6723 delete_post_meta( $post_id, '_wp_trash_meta_status' );
6724 delete_post_meta( $post_id, '_wp_trash_meta_time' );
6725
6726 $meta = wp_get_attachment_metadata( $post_id );
6727 $backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
6728 $file = get_attached_file( $post_id );
6729
6730 if ( is_multisite() && is_string( $file ) && ! empty( $file ) ) {
6731 clean_dirsize_cache( $file );
6732 }
6733
6734 /**
6735 * Fires before an attachment is deleted, at the start of wp_delete_attachment().
6736 *
6737 * @since 2.0.0
6738 * @since 5.5.0 Added the `$post` parameter.
6739 *
6740 * @param int $post_id Attachment ID.
6741 * @param WP_Post $post Post object.
6742 */
6743 do_action( 'delete_attachment', $post_id, $post );
6744
6745 wp_delete_object_term_relationships( $post_id, array( 'category', 'post_tag' ) );
6746 wp_delete_object_term_relationships( $post_id, get_object_taxonomies( $post->post_type ) );
6747
6748 // Delete all for any posts.
6749 delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
6750
6751 wp_defer_comment_counting( true );
6752
6753 $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $post_id ) );
6754 foreach ( $comment_ids as $comment_id ) {
6755 wp_delete_comment( $comment_id, true );
6756 }
6757
6758 wp_defer_comment_counting( false );
6759
6760 $post_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $post_id ) );
6761 foreach ( $post_meta_ids as $mid ) {
6762 delete_metadata_by_mid( 'post', $mid );
6763 }
6764
6765 /** This action is documented in wp-includes/post.php */
6766 do_action( 'delete_post', $post_id, $post );
6767 $result = $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) );
6768 if ( ! $result ) {
6769 return false;
6770 }
6771 /** This action is documented in wp-includes/post.php */
6772 do_action( 'deleted_post', $post_id, $post );
6773
6774 wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file );
6775
6776 clean_post_cache( $post );
6777
6778 return $post;
6779}
6780
6781/**
6782 * Deletes all files that belong to the given attachment.
6783 *
6784 * @since 4.9.7
6785 *
6786 * @global wpdb $wpdb WordPress database abstraction object.
6787 *
6788 * @param int $post_id Attachment ID.
6789 * @param array $meta The attachment's meta data.
6790 * @param array $backup_sizes The meta data for the attachment's backup images.
6791 * @param string $file Absolute path to the attachment's file.
6792 * @return bool True on success, false on failure.
6793 */
6794function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) {
6795 global $wpdb;
6796
6797 $uploadpath = wp_get_upload_dir();
6798 $deleted = true;
6799
6800 if ( ! empty( $meta['thumb'] ) ) {
6801 // Don't delete the thumb if another attachment uses it.
6802 if ( ! $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND meta_value LIKE %s AND post_id <> %d", '%' . $wpdb->esc_like( $meta['thumb'] ) . '%', $post_id ) ) ) {
6803 $thumbfile = str_replace( wp_basename( $file ), $meta['thumb'], $file );
6804
6805 if ( ! empty( $thumbfile ) ) {
6806 $thumbfile = path_join( $uploadpath['basedir'], $thumbfile );
6807 $thumbdir = path_join( $uploadpath['basedir'], dirname( $file ) );
6808
6809 if ( ! wp_delete_file_from_directory( $thumbfile, $thumbdir ) ) {
6810 $deleted = false;
6811 }
6812 }
6813 }
6814 }
6815
6816 // Remove intermediate and backup images if there are any.
6817 if ( isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) {
6818 $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) );
6819
6820 foreach ( $meta['sizes'] as $size => $sizeinfo ) {
6821 $intermediate_file = str_replace( wp_basename( $file ), $sizeinfo['file'], $file );
6822
6823 if ( ! empty( $intermediate_file ) ) {
6824 $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file );
6825
6826 if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) {
6827 $deleted = false;
6828 }
6829 }
6830 }
6831 }
6832
6833 if ( ! empty( $meta['original_image'] ) ) {
6834 if ( empty( $intermediate_dir ) ) {
6835 $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) );
6836 }
6837
6838 $original_image = str_replace( wp_basename( $file ), $meta['original_image'], $file );
6839
6840 if ( ! empty( $original_image ) ) {
6841 $original_image = path_join( $uploadpath['basedir'], $original_image );
6842
6843 if ( ! wp_delete_file_from_directory( $original_image, $intermediate_dir ) ) {
6844 $deleted = false;
6845 }
6846 }
6847 }
6848
6849 if ( is_array( $backup_sizes ) ) {
6850 $del_dir = path_join( $uploadpath['basedir'], dirname( $meta['file'] ) );
6851
6852 foreach ( $backup_sizes as $size ) {
6853 $del_file = path_join( dirname( $meta['file'] ), $size['file'] );
6854
6855 if ( ! empty( $del_file ) ) {
6856 $del_file = path_join( $uploadpath['basedir'], $del_file );
6857
6858 if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) {
6859 $deleted = false;
6860 }
6861 }
6862 }
6863 }
6864
6865 if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) {
6866 $deleted = false;
6867 }
6868
6869 return $deleted;
6870}
6871
6872/**
6873 * Retrieves attachment metadata for attachment ID.
6874 *
6875 * @since 2.1.0
6876 * @since 6.0.0 The `$filesize` value was added to the returned array.
6877 *
6878 * @param int $attachment_id Attachment post ID. Defaults to global $post.
6879 * @param bool $unfiltered Optional. If true, filters are not run. Default false.
6880 * @return array|false {
6881 * Attachment metadata. False on failure.
6882 *
6883 * @type int $width The width of the attachment.
6884 * @type int $height The height of the attachment.
6885 * @type string $file The file path relative to `wp-content/uploads`.
6886 * @type array $sizes Keys are size slugs, each value is an array containing
6887 * 'file', 'width', 'height', and 'mime-type'.
6888 * @type array $image_meta Image metadata.
6889 * @type int $filesize File size of the attachment.
6890 * }
6891 */
6892function wp_get_attachment_metadata( $attachment_id = 0, $unfiltered = false ) {
6893 $attachment_id = (int) $attachment_id;
6894
6895 if ( ! $attachment_id ) {
6896 $post = get_post();
6897
6898 if ( ! $post ) {
6899 return false;
6900 }
6901
6902 $attachment_id = $post->ID;
6903 }
6904
6905 $data = get_post_meta( $attachment_id, '_wp_attachment_metadata', true );
6906
6907 if ( ! $data ) {
6908 return false;
6909 }
6910
6911 if ( $unfiltered ) {
6912 return $data;
6913 }
6914
6915 /**
6916 * Filters the attachment meta data.
6917 *
6918 * @since 2.1.0
6919 *
6920 * @param array $data Array of meta data for the given attachment.
6921 * @param int $attachment_id Attachment post ID.
6922 */
6923 return apply_filters( 'wp_get_attachment_metadata', $data, $attachment_id );
6924}
6925
6926/**
6927 * Updates metadata for an attachment.
6928 *
6929 * @since 2.1.0
6930 *
6931 * @param int $attachment_id Attachment post ID.
6932 * @param array $data Attachment meta data.
6933 * @return int|bool Whether the metadata was successfully updated.
6934 * True on success, the Meta ID if the key didn't exist.
6935 * False if $post is invalid, on failure, or if $data is the same as the existing metadata.
6936 */
6937function wp_update_attachment_metadata( $attachment_id, $data ) {
6938 $attachment_id = (int) $attachment_id;
6939
6940 $post = get_post( $attachment_id );
6941
6942 if ( ! $post ) {
6943 return false;
6944 }
6945
6946 /**
6947 * Filters the updated attachment meta data.
6948 *
6949 * @since 2.1.0
6950 *
6951 * @param array $data Array of updated attachment meta data.
6952 * @param int $attachment_id Attachment post ID.
6953 */
6954 $data = apply_filters( 'wp_update_attachment_metadata', $data, $post->ID );
6955 if ( $data ) {
6956 return update_post_meta( $post->ID, '_wp_attachment_metadata', $data );
6957 } else {
6958 return delete_post_meta( $post->ID, '_wp_attachment_metadata' );
6959 }
6960}
6961
6962/**
6963 * Retrieves the URL for an attachment.
6964 *
6965 * @since 2.1.0
6966 *
6967 * @global string $pagenow The filename of the current screen.
6968 *
6969 * @param int $attachment_id Optional. Attachment post ID. Defaults to global $post.
6970 * @return string|false Attachment URL, otherwise false.
6971 */
6972function wp_get_attachment_url( $attachment_id = 0 ) {
6973 global $pagenow;
6974
6975 $attachment_id = (int) $attachment_id;
6976
6977 $post = get_post( $attachment_id );
6978
6979 if ( ! $post ) {
6980 return false;
6981 }
6982
6983 if ( 'attachment' !== $post->post_type ) {
6984 return false;
6985 }
6986
6987 $url = '';
6988 // Get attached file.
6989 $file = get_post_meta( $post->ID, '_wp_attached_file', true );
6990 if ( $file ) {
6991 // Get upload directory.
6992 $uploads = wp_get_upload_dir();
6993 if ( $uploads && false === $uploads['error'] ) {
6994 // Check that the upload base exists in the file location.
6995 if ( str_starts_with( $file, $uploads['basedir'] ) ) {
6996 // Replace file location with url location.
6997 $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file );
6998 } elseif ( str_contains( $file, 'wp-content/uploads' ) ) {
6999 // Get the directory name relative to the basedir (back compat for pre-2.7 uploads).
7000 $url = trailingslashit( $uploads['baseurl'] . '/' . _wp_get_attachment_relative_path( $file ) ) . wp_basename( $file );
7001 } else {
7002 // It's a newly-uploaded file, therefore $file is relative to the basedir.
7003 $url = $uploads['baseurl'] . "/$file";
7004 }
7005 }
7006 }
7007
7008 /*
7009 * If any of the above options failed, Fallback on the GUID as used pre-2.7,
7010 * not recommended to rely upon this.
7011 */
7012 if ( ! $url ) {
7013 $url = get_the_guid( $post->ID );
7014 }
7015
7016 // On SSL front end, URLs should be HTTPS.
7017 if ( is_ssl() && ! is_admin() && 'wp-login.php' !== $pagenow ) {
7018 $url = set_url_scheme( $url );
7019 }
7020
7021 /**
7022 * Filters the attachment URL.
7023 *
7024 * @since 2.1.0
7025 *
7026 * @param string $url URL for the given attachment.
7027 * @param int $attachment_id Attachment post ID.
7028 */
7029 $url = apply_filters( 'wp_get_attachment_url', $url, $post->ID );
7030
7031 if ( ! $url ) {
7032 return false;
7033 }
7034
7035 return $url;
7036}
7037
7038/**
7039 * Retrieves the caption for an attachment.
7040 *
7041 * @since 4.6.0
7042 *
7043 * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`.
7044 * @return string|false Attachment caption on success, false on failure.
7045 */
7046function wp_get_attachment_caption( $post_id = 0 ) {
7047 $post_id = (int) $post_id;
7048 $post = get_post( $post_id );
7049
7050 if ( ! $post ) {
7051 return false;
7052 }
7053
7054 if ( 'attachment' !== $post->post_type ) {
7055 return false;
7056 }
7057
7058 $caption = $post->post_excerpt;
7059
7060 /**
7061 * Filters the attachment caption.
7062 *
7063 * @since 4.6.0
7064 *
7065 * @param string $caption Caption for the given attachment.
7066 * @param int $post_id Attachment ID.
7067 */
7068 return apply_filters( 'wp_get_attachment_caption', $caption, $post->ID );
7069}
7070
7071/**
7072 * Retrieves URL for an attachment thumbnail.
7073 *
7074 * @since 2.1.0
7075 * @since 6.1.0 Changed to use wp_get_attachment_image_url().
7076 *
7077 * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`.
7078 * @return string|false Thumbnail URL on success, false on failure.
7079 */
7080function wp_get_attachment_thumb_url( $post_id = 0 ) {
7081 $post_id = (int) $post_id;
7082
7083 /*
7084 * This uses image_downsize() which also looks for the (very) old format $image_meta['thumb']
7085 * when the newer format $image_meta['sizes']['thumbnail'] doesn't exist.
7086 */
7087 $thumbnail_url = wp_get_attachment_image_url( $post_id, 'thumbnail' );
7088
7089 if ( empty( $thumbnail_url ) ) {
7090 return false;
7091 }
7092
7093 /**
7094 * Filters the attachment thumbnail URL.
7095 *
7096 * @since 2.1.0
7097 *
7098 * @param string $thumbnail_url URL for the attachment thumbnail.
7099 * @param int $post_id Attachment ID.
7100 */
7101 return apply_filters( 'wp_get_attachment_thumb_url', $thumbnail_url, $post_id );
7102}
7103
7104/**
7105 * Verifies an attachment is of a given type.
7106 *
7107 * @since 4.2.0
7108 *
7109 * @param string $type Attachment type. Accepts `image`, `audio`, `video`, or a file extension.
7110 * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post.
7111 * @return bool True if an accepted type or a matching file extension, false otherwise.
7112 */
7113function wp_attachment_is( $type, $post = null ) {
7114 $post = get_post( $post );
7115
7116 if ( ! $post ) {
7117 return false;
7118 }
7119
7120 $file = get_attached_file( $post->ID );
7121
7122 if ( ! $file ) {
7123 return false;
7124 }
7125
7126 if ( str_starts_with( $post->post_mime_type, $type . '/' ) ) {
7127 return true;
7128 }
7129
7130 $check = wp_check_filetype( $file );
7131
7132 if ( empty( $check['ext'] ) ) {
7133 return false;
7134 }
7135
7136 $ext = $check['ext'];
7137
7138 if ( 'import' !== $post->post_mime_type ) {
7139 return $type === $ext;
7140 }
7141
7142 switch ( $type ) {
7143 case 'image':
7144 $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif', 'heic' );
7145 return in_array( $ext, $image_exts, true );
7146
7147 case 'audio':
7148 return in_array( $ext, wp_get_audio_extensions(), true );
7149
7150 case 'video':
7151 return in_array( $ext, wp_get_video_extensions(), true );
7152
7153 default:
7154 return $type === $ext;
7155 }
7156}
7157
7158/**
7159 * Determines whether an attachment is an image.
7160 *
7161 * For more information on this and similar theme functions, check out
7162 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
7163 * Conditional Tags} article in the Theme Developer Handbook.
7164 *
7165 * @since 2.1.0
7166 * @since 4.2.0 Modified into wrapper for wp_attachment_is() and
7167 * allowed WP_Post object to be passed.
7168 *
7169 * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post.
7170 * @return bool Whether the attachment is an image.
7171 */
7172function wp_attachment_is_image( $post = null ) {
7173 return wp_attachment_is( 'image', $post );
7174}
7175
7176/**
7177 * Retrieves the icon for a MIME type or attachment.
7178 *
7179 * @since 2.1.0
7180 * @since 6.5.0 Added the `$preferred_ext` parameter.
7181 *
7182 * @param string|int $mime MIME type or attachment ID.
7183 * @param string $preferred_ext File format to prefer in return. Default '.png'.
7184 * @return string|false Icon, false otherwise.
7185 */
7186function wp_mime_type_icon( $mime = 0, $preferred_ext = '.png' ) {
7187 if ( ! is_numeric( $mime ) ) {
7188 $icon = wp_cache_get( "mime_type_icon_$mime" );
7189 }
7190
7191 // Check if preferred file format variable is present and is a validly formatted file extension.
7192 if ( ! empty( $preferred_ext ) && is_string( $preferred_ext ) && ! str_starts_with( $preferred_ext, '.' ) ) {
7193 $preferred_ext = '.' . strtolower( $preferred_ext );
7194 }
7195
7196 $post_id = 0;
7197 if ( empty( $icon ) ) {
7198 $post_mimes = array();
7199 if ( is_numeric( $mime ) ) {
7200 $mime = (int) $mime;
7201 $post = get_post( $mime );
7202 if ( $post ) {
7203 $post_id = (int) $post->ID;
7204 $file = get_attached_file( $post_id );
7205 $ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $file );
7206 if ( ! empty( $ext ) ) {
7207 $post_mimes[] = $ext;
7208 $ext_type = wp_ext2type( $ext );
7209 if ( $ext_type ) {
7210 $post_mimes[] = $ext_type;
7211 }
7212 }
7213 $mime = $post->post_mime_type;
7214 } else {
7215 $mime = 0;
7216 }
7217 } else {
7218 $post_mimes[] = $mime;
7219 }
7220
7221 $icon_files = wp_cache_get( 'icon_files' );
7222
7223 if ( ! is_array( $icon_files ) ) {
7224 /**
7225 * Filters the icon directory path.
7226 *
7227 * @since 2.0.0
7228 *
7229 * @param string $path Icon directory absolute path.
7230 */
7231 $icon_dir = apply_filters( 'icon_dir', ABSPATH . WPINC . '/images/media' );
7232
7233 /**
7234 * Filters the icon directory URI.
7235 *
7236 * @since 2.0.0
7237 *
7238 * @param string $uri Icon directory URI.
7239 */
7240 $icon_dir_uri = apply_filters( 'icon_dir_uri', includes_url( 'images/media' ) );
7241
7242 /**
7243 * Filters the array of icon directory URIs.
7244 *
7245 * @since 2.5.0
7246 *
7247 * @param string[] $uris Array of icon directory URIs keyed by directory absolute path.
7248 */
7249 $dirs = apply_filters( 'icon_dirs', array( $icon_dir => $icon_dir_uri ) );
7250 $icon_files = array();
7251 $all_icons = array();
7252 while ( $dirs ) {
7253 $keys = array_keys( $dirs );
7254 $dir = array_shift( $keys );
7255 $uri = array_shift( $dirs );
7256 $dh = opendir( $dir );
7257 if ( $dh ) {
7258 while ( false !== $file = readdir( $dh ) ) {
7259 $file = wp_basename( $file );
7260 if ( str_starts_with( $file, '.' ) ) {
7261 continue;
7262 }
7263
7264 $ext = strtolower( substr( $file, -4 ) );
7265 if ( ! in_array( $ext, array( '.svg', '.png', '.gif', '.jpg' ), true ) ) {
7266 if ( is_dir( "$dir/$file" ) ) {
7267 $dirs[ "$dir/$file" ] = "$uri/$file";
7268 }
7269 continue;
7270 }
7271 $all_icons[ "$dir/$file" ] = "$uri/$file";
7272 if ( $ext === $preferred_ext ) {
7273 $icon_files[ "$dir/$file" ] = "$uri/$file";
7274 }
7275 }
7276 closedir( $dh );
7277 }
7278 }
7279 // If directory only contained icons of a non-preferred format, return those.
7280 if ( empty( $icon_files ) ) {
7281 $icon_files = $all_icons;
7282 }
7283 wp_cache_add( 'icon_files', $icon_files, 'default', 600 );
7284 }
7285
7286 $types = array();
7287 // Icon wp_basename - extension = MIME wildcard.
7288 foreach ( $icon_files as $file => $uri ) {
7289 $types[ preg_replace( '/^([^.]*).*$/', '$1', wp_basename( $file ) ) ] =& $icon_files[ $file ];
7290 }
7291
7292 if ( ! empty( $mime ) ) {
7293 $post_mimes[] = substr( $mime, 0, strpos( $mime, '/' ) );
7294 $post_mimes[] = substr( $mime, strpos( $mime, '/' ) + 1 );
7295 $post_mimes[] = str_replace( '/', '_', $mime );
7296 }
7297
7298 $matches = wp_match_mime_types( array_keys( $types ), $post_mimes );
7299 $matches['default'] = array( 'default' );
7300
7301 foreach ( $matches as $match => $wilds ) {
7302 foreach ( $wilds as $wild ) {
7303 if ( ! isset( $types[ $wild ] ) ) {
7304 continue;
7305 }
7306
7307 $icon = $types[ $wild ];
7308 if ( ! is_numeric( $mime ) ) {
7309 wp_cache_add( "mime_type_icon_$mime", $icon );
7310 }
7311 break 2;
7312 }
7313 }
7314 }
7315
7316 /**
7317 * Filters the mime type icon.
7318 *
7319 * @since 2.1.0
7320 *
7321 * @param string $icon Path to the mime type icon.
7322 * @param string $mime Mime type.
7323 * @param int $post_id Attachment ID. Will equal 0 if the function passed
7324 * the mime type.
7325 */
7326 return apply_filters( 'wp_mime_type_icon', $icon, $mime, $post_id );
7327}
7328
7329/**
7330 * Checks for changed slugs for published post objects and save the old slug.
7331 *
7332 * The function is used when a post object of any type is updated,
7333 * by comparing the current and previous post objects.
7334 *
7335 * If the slug was changed and not already part of the old slugs then it will be
7336 * added to the post meta field ('_wp_old_slug') for storing old slugs for that
7337 * post.
7338 *
7339 * The most logically usage of this function is redirecting changed post objects, so
7340 * that those that linked to an changed post will be redirected to the new post.
7341 *
7342 * @since 2.1.0
7343 *
7344 * @param int $post_id Post ID.
7345 * @param WP_Post $post The post object.
7346 * @param WP_Post $post_before The previous post object.
7347 */
7348function wp_check_for_changed_slugs( $post_id, $post, $post_before ) {
7349 // Don't bother if it hasn't changed.
7350 if ( $post->post_name === $post_before->post_name ) {
7351 return;
7352 }
7353
7354 // We're only concerned with published, non-hierarchical objects.
7355 if ( ! ( 'publish' === $post->post_status || ( 'attachment' === $post->post_type && 'inherit' === $post->post_status ) )
7356 || is_post_type_hierarchical( $post->post_type )
7357 ) {
7358 return;
7359 }
7360
7361 $old_slugs = (array) get_post_meta( $post_id, '_wp_old_slug' );
7362
7363 // If we haven't added this old slug before, add it now.
7364 if ( ! empty( $post_before->post_name ) && ! in_array( $post_before->post_name, $old_slugs, true ) ) {
7365 add_post_meta( $post_id, '_wp_old_slug', $post_before->post_name );
7366 }
7367
7368 // If the new slug was used previously, delete it from the list.
7369 if ( in_array( $post->post_name, $old_slugs, true ) ) {
7370 delete_post_meta( $post_id, '_wp_old_slug', $post->post_name );
7371 }
7372}
7373
7374/**
7375 * Checks for changed dates for published post objects and save the old date.
7376 *
7377 * The function is used when a post object of any type is updated,
7378 * by comparing the current and previous post objects.
7379 *
7380 * If the date was changed and not already part of the old dates then it will be
7381 * added to the post meta field ('_wp_old_date') for storing old dates for that
7382 * post.
7383 *
7384 * The most logically usage of this function is redirecting changed post objects, so
7385 * that those that linked to an changed post will be redirected to the new post.
7386 *
7387 * @since 4.9.3
7388 *
7389 * @param int $post_id Post ID.
7390 * @param WP_Post $post The post object.
7391 * @param WP_Post $post_before The previous post object.
7392 */
7393function wp_check_for_changed_dates( $post_id, $post, $post_before ) {
7394 $previous_date = gmdate( 'Y-m-d', strtotime( $post_before->post_date ) );
7395 $new_date = gmdate( 'Y-m-d', strtotime( $post->post_date ) );
7396
7397 // Don't bother if it hasn't changed.
7398 if ( $new_date === $previous_date ) {
7399 return;
7400 }
7401
7402 // We're only concerned with published, non-hierarchical objects.
7403 if ( ! ( 'publish' === $post->post_status || ( 'attachment' === $post->post_type && 'inherit' === $post->post_status ) )
7404 || is_post_type_hierarchical( $post->post_type )
7405 ) {
7406 return;
7407 }
7408
7409 $old_dates = (array) get_post_meta( $post_id, '_wp_old_date' );
7410
7411 // If we haven't added this old date before, add it now.
7412 if ( ! empty( $previous_date ) && ! in_array( $previous_date, $old_dates, true ) ) {
7413 add_post_meta( $post_id, '_wp_old_date', $previous_date );
7414 }
7415
7416 // If the new slug was used previously, delete it from the list.
7417 if ( in_array( $new_date, $old_dates, true ) ) {
7418 delete_post_meta( $post_id, '_wp_old_date', $new_date );
7419 }
7420}
7421
7422/**
7423 * Retrieves the private post SQL based on capability.
7424 *
7425 * This function provides a standardized way to appropriately select on the
7426 * post_status of a post type. The function will return a piece of SQL code
7427 * that can be added to a WHERE clause; this SQL is constructed to allow all
7428 * published posts, and all private posts to which the user has access.
7429 *
7430 * @since 2.2.0
7431 * @since 4.3.0 Added the ability to pass an array to `$post_type`.
7432 *
7433 * @param string|array $post_type Single post type or an array of post types. Currently only supports 'post' or 'page'.
7434 * @return string SQL code that can be added to a where clause.
7435 */
7436function get_private_posts_cap_sql( $post_type ) {
7437 return get_posts_by_author_sql( $post_type, false );
7438}
7439
7440/**
7441 * Retrieves the post SQL based on capability, author, and type.
7442 *
7443 * @since 3.0.0
7444 * @since 4.3.0 Introduced the ability to pass an array of post types to `$post_type`.
7445 *
7446 * @see get_private_posts_cap_sql()
7447 * @global wpdb $wpdb WordPress database abstraction object.
7448 *
7449 * @param string|string[] $post_type Single post type or an array of post types.
7450 * @param bool $full Optional. Returns a full WHERE statement instead of just
7451 * an 'andalso' term. Default true.
7452 * @param int $post_author Optional. Query posts having a single author ID. Default null.
7453 * @param bool $public_only Optional. Only return public posts. Skips cap checks for
7454 * $current_user. Default false.
7455 * @return string SQL WHERE code that can be added to a query.
7456 */
7457function get_posts_by_author_sql( $post_type, $full = true, $post_author = null, $public_only = false ) {
7458 global $wpdb;
7459
7460 if ( is_array( $post_type ) ) {
7461 $post_types = $post_type;
7462 } else {
7463 $post_types = array( $post_type );
7464 }
7465
7466 $post_type_clauses = array();
7467 foreach ( $post_types as $post_type ) {
7468 $post_type_obj = get_post_type_object( $post_type );
7469
7470 if ( ! $post_type_obj ) {
7471 continue;
7472 }
7473
7474 /**
7475 * Filters the capability to read private posts for a custom post type
7476 * when generating SQL for getting posts by author.
7477 *
7478 * @since 2.2.0
7479 * @deprecated 3.2.0 The hook transitioned from "somewhat useless" to "totally useless".
7480 *
7481 * @param string $cap Capability.
7482 */
7483 $cap = apply_filters_deprecated( 'pub_priv_sql_capability', array( '' ), '3.2.0' );
7484
7485 if ( ! $cap ) {
7486 $cap = current_user_can( $post_type_obj->cap->read_private_posts );
7487 }
7488
7489 // Only need to check the cap if $public_only is false.
7490 $post_status_sql = "post_status = 'publish'";
7491
7492 if ( false === $public_only ) {
7493 if ( $cap ) {
7494 // Does the user have the capability to view private posts? Guess so.
7495 $post_status_sql .= " OR post_status = 'private'";
7496 } elseif ( is_user_logged_in() ) {
7497 // Users can view their own private posts.
7498 $id = get_current_user_id();
7499 if ( null === $post_author || ! $full ) {
7500 $post_status_sql .= " OR post_status = 'private' AND post_author = $id";
7501 } elseif ( $id === (int) $post_author ) {
7502 $post_status_sql .= " OR post_status = 'private'";
7503 } // Else none.
7504 } // Else none.
7505 }
7506
7507 $post_type_clauses[] = "( post_type = '" . $post_type . "' AND ( $post_status_sql ) )";
7508 }
7509
7510 if ( empty( $post_type_clauses ) ) {
7511 return $full ? 'WHERE 1 = 0' : '1 = 0';
7512 }
7513
7514 $sql = '( ' . implode( ' OR ', $post_type_clauses ) . ' )';
7515
7516 if ( null !== $post_author ) {
7517 $sql .= $wpdb->prepare( ' AND post_author = %d', $post_author );
7518 }
7519
7520 if ( $full ) {
7521 $sql = 'WHERE ' . $sql;
7522 }
7523
7524 return $sql;
7525}
7526
7527/**
7528 * Retrieves the most recent time that a post on the site was published.
7529 *
7530 * The server timezone is the default and is the difference between GMT and
7531 * server time. The 'blog' value is the date when the last post was posted.
7532 * The 'gmt' is when the last post was posted in GMT formatted date.
7533 *
7534 * @since 0.71
7535 * @since 4.4.0 The `$post_type` argument was added.
7536 *
7537 * @param string $timezone Optional. The timezone for the timestamp. Accepts 'server', 'blog', or 'gmt'.
7538 * 'server' uses the server's internal timezone.
7539 * 'blog' uses the `post_date` field, which proxies to the timezone set for the site.
7540 * 'gmt' uses the `post_date_gmt` field.
7541 * Default 'server'.
7542 * @param string $post_type Optional. The post type to check. Default 'any'.
7543 * @return string The date of the last post, or false on failure.
7544 */
7545function get_lastpostdate( $timezone = 'server', $post_type = 'any' ) {
7546 $lastpostdate = _get_last_post_time( $timezone, 'date', $post_type );
7547
7548 /**
7549 * Filters the most recent time that a post on the site was published.
7550 *
7551 * @since 2.3.0
7552 * @since 5.5.0 Added the `$post_type` parameter.
7553 *
7554 * @param string|false $lastpostdate The most recent time that a post was published,
7555 * in 'Y-m-d H:i:s' format. False on failure.
7556 * @param string $timezone Location to use for getting the post published date.
7557 * See get_lastpostdate() for accepted `$timezone` values.
7558 * @param string $post_type The post type to check.
7559 */
7560 return apply_filters( 'get_lastpostdate', $lastpostdate, $timezone, $post_type );
7561}
7562
7563/**
7564 * Gets the most recent time that a post on the site was modified.
7565 *
7566 * The server timezone is the default and is the difference between GMT and
7567 * server time. The 'blog' value is just when the last post was modified.
7568 * The 'gmt' is when the last post was modified in GMT time.
7569 *
7570 * @since 1.2.0
7571 * @since 4.4.0 The `$post_type` argument was added.
7572 *
7573 * @param string $timezone Optional. The timezone for the timestamp. See get_lastpostdate()
7574 * for information on accepted values.
7575 * Default 'server'.
7576 * @param string $post_type Optional. The post type to check. Default 'any'.
7577 * @return string The timestamp in 'Y-m-d H:i:s' format, or false on failure.
7578 */
7579function get_lastpostmodified( $timezone = 'server', $post_type = 'any' ) {
7580 /**
7581 * Pre-filter the return value of get_lastpostmodified() before the query is run.
7582 *
7583 * @since 4.4.0
7584 *
7585 * @param string|false $lastpostmodified The most recent time that a post was modified,
7586 * in 'Y-m-d H:i:s' format, or false. Returning anything
7587 * other than false will short-circuit the function.
7588 * @param string $timezone Location to use for getting the post modified date.
7589 * See get_lastpostdate() for accepted `$timezone` values.
7590 * @param string $post_type The post type to check.
7591 */
7592 $lastpostmodified = apply_filters( 'pre_get_lastpostmodified', false, $timezone, $post_type );
7593
7594 if ( false !== $lastpostmodified ) {
7595 return $lastpostmodified;
7596 }
7597
7598 $lastpostmodified = _get_last_post_time( $timezone, 'modified', $post_type );
7599 $lastpostdate = get_lastpostdate( $timezone, $post_type );
7600
7601 if ( $lastpostdate > $lastpostmodified ) {
7602 $lastpostmodified = $lastpostdate;
7603 }
7604
7605 /**
7606 * Filters the most recent time that a post on the site was modified.
7607 *
7608 * @since 2.3.0
7609 * @since 5.5.0 Added the `$post_type` parameter.
7610 *
7611 * @param string|false $lastpostmodified The most recent time that a post was modified,
7612 * in 'Y-m-d H:i:s' format. False on failure.
7613 * @param string $timezone Location to use for getting the post modified date.
7614 * See get_lastpostdate() for accepted `$timezone` values.
7615 * @param string $post_type The post type to check.
7616 */
7617 return apply_filters( 'get_lastpostmodified', $lastpostmodified, $timezone, $post_type );
7618}
7619
7620/**
7621 * Gets the timestamp of the last time any post was modified or published.
7622 *
7623 * @since 3.1.0
7624 * @since 4.4.0 The `$post_type` argument was added.
7625 * @access private
7626 *
7627 * @global wpdb $wpdb WordPress database abstraction object.
7628 *
7629 * @param string $timezone The timezone for the timestamp. See get_lastpostdate().
7630 * for information on accepted values.
7631 * @param string $field Post field to check. Accepts 'date' or 'modified'.
7632 * @param string $post_type Optional. The post type to check. Default 'any'.
7633 * @return string|false The timestamp in 'Y-m-d H:i:s' format, or false on failure.
7634 */
7635function _get_last_post_time( $timezone, $field, $post_type = 'any' ) {
7636 global $wpdb;
7637
7638 if ( ! in_array( $field, array( 'date', 'modified' ), true ) ) {
7639 return false;
7640 }
7641
7642 $timezone = strtolower( $timezone );
7643
7644 $key = "lastpost{$field}:$timezone";
7645 if ( 'any' !== $post_type ) {
7646 $key .= ':' . sanitize_key( $post_type );
7647 }
7648
7649 $date = wp_cache_get( $key, 'timeinfo' );
7650 if ( false !== $date ) {
7651 return $date;
7652 }
7653
7654 if ( 'any' === $post_type ) {
7655 $post_types = get_post_types( array( 'public' => true ) );
7656 array_walk( $post_types, array( $wpdb, 'escape_by_ref' ) );
7657 $post_types = "'" . implode( "', '", $post_types ) . "'";
7658 } else {
7659 $post_types = "'" . sanitize_key( $post_type ) . "'";
7660 }
7661
7662 switch ( $timezone ) {
7663 case 'gmt':
7664 $date = $wpdb->get_var( "SELECT post_{$field}_gmt FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" );
7665 break;
7666 case 'blog':
7667 $date = $wpdb->get_var( "SELECT post_{$field} FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" );
7668 break;
7669 case 'server':
7670 $add_seconds_server = gmdate( 'Z' );
7671 $date = $wpdb->get_var( "SELECT DATE_ADD(post_{$field}_gmt, INTERVAL '$add_seconds_server' SECOND) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" );
7672 break;
7673 }
7674
7675 if ( $date ) {
7676 wp_cache_set( $key, $date, 'timeinfo' );
7677
7678 return $date;
7679 }
7680
7681 return false;
7682}
7683
7684/**
7685 * Updates posts in cache.
7686 *
7687 * @since 1.5.1
7688 *
7689 * @param WP_Post[] $posts Array of post objects (passed by reference).
7690 */
7691function update_post_cache( &$posts ) {
7692 if ( ! $posts ) {
7693 return;
7694 }
7695
7696 $data = array();
7697 foreach ( $posts as $post ) {
7698 if ( empty( $post->filter ) || 'raw' !== $post->filter ) {
7699 $post = sanitize_post( $post, 'raw' );
7700 }
7701 $data[ $post->ID ] = $post;
7702 }
7703 wp_cache_add_multiple( $data, 'posts' );
7704}
7705
7706/**
7707 * Will clean the post in the cache.
7708 *
7709 * Cleaning means delete from the cache of the post. Will call to clean the term
7710 * object cache associated with the post ID.
7711 *
7712 * This function not run if $_wp_suspend_cache_invalidation is not empty. See
7713 * wp_suspend_cache_invalidation().
7714 *
7715 * @since 2.0.0
7716 *
7717 * @global bool $_wp_suspend_cache_invalidation
7718 *
7719 * @param int|WP_Post $post Post ID or post object to remove from the cache.
7720 */
7721function clean_post_cache( $post ) {
7722 global $_wp_suspend_cache_invalidation;
7723
7724 if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
7725 return;
7726 }
7727
7728 $post = get_post( $post );
7729
7730 if ( ! $post ) {
7731 return;
7732 }
7733
7734 wp_cache_delete( $post->ID, 'posts' );
7735 wp_cache_delete( 'post_parent:' . (string) $post->ID, 'posts' );
7736 wp_cache_delete( $post->ID, 'post_meta' );
7737
7738 clean_object_term_cache( $post->ID, $post->post_type );
7739
7740 wp_cache_delete( 'wp_get_archives', 'general' );
7741
7742 /**
7743 * Fires immediately after the given post's cache is cleaned.
7744 *
7745 * @since 2.5.0
7746 *
7747 * @param int $post_id Post ID.
7748 * @param WP_Post $post Post object.
7749 */
7750 do_action( 'clean_post_cache', $post->ID, $post );
7751
7752 if ( 'page' === $post->post_type ) {
7753 wp_cache_delete( 'all_page_ids', 'posts' );
7754
7755 /**
7756 * Fires immediately after the given page's cache is cleaned.
7757 *
7758 * @since 2.5.0
7759 *
7760 * @param int $post_id Post ID.
7761 */
7762 do_action( 'clean_page_cache', $post->ID );
7763 }
7764
7765 wp_cache_set_posts_last_changed();
7766}
7767
7768/**
7769 * Updates post, term, and metadata caches for a list of post objects.
7770 *
7771 * @since 1.5.0
7772 *
7773 * @param WP_Post[] $posts Array of post objects (passed by reference).
7774 * @param string $post_type Optional. Post type. Default 'post'.
7775 * @param bool $update_term_cache Optional. Whether to update the term cache. Default true.
7776 * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true.
7777 */
7778function update_post_caches( &$posts, $post_type = 'post', $update_term_cache = true, $update_meta_cache = true ) {
7779 // No point in doing all this work if we didn't match any posts.
7780 if ( ! $posts ) {
7781 return;
7782 }
7783
7784 update_post_cache( $posts );
7785
7786 $post_ids = array();
7787 foreach ( $posts as $post ) {
7788 $post_ids[] = $post->ID;
7789 }
7790
7791 if ( ! $post_type ) {
7792 $post_type = 'any';
7793 }
7794
7795 if ( $update_term_cache ) {
7796 if ( is_array( $post_type ) ) {
7797 $ptypes = $post_type;
7798 } elseif ( 'any' === $post_type ) {
7799 $ptypes = array();
7800 // Just use the post_types in the supplied posts.
7801 foreach ( $posts as $post ) {
7802 $ptypes[] = $post->post_type;
7803 }
7804 $ptypes = array_unique( $ptypes );
7805 } else {
7806 $ptypes = array( $post_type );
7807 }
7808
7809 if ( ! empty( $ptypes ) ) {
7810 update_object_term_cache( $post_ids, $ptypes );
7811 }
7812 }
7813
7814 if ( $update_meta_cache ) {
7815 update_postmeta_cache( $post_ids );
7816 }
7817}
7818
7819/**
7820 * Updates post author user caches for a list of post objects.
7821 *
7822 * @since 6.1.0
7823 *
7824 * @param WP_Post[] $posts Array of post objects.
7825 */
7826function update_post_author_caches( $posts ) {
7827 /*
7828 * cache_users() is a pluggable function so is not available prior
7829 * to the `plugins_loaded` hook firing. This is to ensure against
7830 * fatal errors when the function is not available.
7831 */
7832 if ( ! function_exists( 'cache_users' ) ) {
7833 return;
7834 }
7835
7836 $author_ids = wp_list_pluck( $posts, 'post_author' );
7837 $author_ids = array_map( 'absint', $author_ids );
7838 $author_ids = array_unique( array_filter( $author_ids ) );
7839
7840 cache_users( $author_ids );
7841}
7842
7843/**
7844 * Updates parent post caches for a list of post objects.
7845 *
7846 * @since 6.1.0
7847 *
7848 * @param WP_Post[] $posts Array of post objects.
7849 */
7850function update_post_parent_caches( $posts ) {
7851 $parent_ids = wp_list_pluck( $posts, 'post_parent' );
7852 $parent_ids = array_map( 'absint', $parent_ids );
7853 $parent_ids = array_unique( array_filter( $parent_ids ) );
7854
7855 if ( ! empty( $parent_ids ) ) {
7856 _prime_post_caches( $parent_ids, false );
7857 }
7858}
7859
7860/**
7861 * Updates metadata cache for a list of post IDs.
7862 *
7863 * Performs SQL query to retrieve the metadata for the post IDs and updates the
7864 * metadata cache for the posts. Therefore, the functions, which call this
7865 * function, do not need to perform SQL queries on their own.
7866 *
7867 * @since 2.1.0
7868 *
7869 * @param int[] $post_ids Array of post IDs.
7870 * @return array|false An array of metadata on success, false if there is nothing to update.
7871 */
7872function update_postmeta_cache( $post_ids ) {
7873 return update_meta_cache( 'post', $post_ids );
7874}
7875
7876/**
7877 * Will clean the attachment in the cache.
7878 *
7879 * Cleaning means delete from the cache. Optionally will clean the term
7880 * object cache associated with the attachment ID.
7881 *
7882 * This function will not run if $_wp_suspend_cache_invalidation is not empty.
7883 *
7884 * @since 3.0.0
7885 *
7886 * @global bool $_wp_suspend_cache_invalidation
7887 *
7888 * @param int $id The attachment ID in the cache to clean.
7889 * @param bool $clean_terms Optional. Whether to clean terms cache. Default false.
7890 */
7891function clean_attachment_cache( $id, $clean_terms = false ) {
7892 global $_wp_suspend_cache_invalidation;
7893
7894 if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
7895 return;
7896 }
7897
7898 $id = (int) $id;
7899
7900 wp_cache_delete( $id, 'posts' );
7901 wp_cache_delete( $id, 'post_meta' );
7902
7903 if ( $clean_terms ) {
7904 clean_object_term_cache( $id, 'attachment' );
7905 }
7906
7907 /**
7908 * Fires after the given attachment's cache is cleaned.
7909 *
7910 * @since 3.0.0
7911 *
7912 * @param int $id Attachment ID.
7913 */
7914 do_action( 'clean_attachment_cache', $id );
7915}
7916
7917//
7918// Hooks.
7919//
7920
7921/**
7922 * Hook for managing future post transitions to published.
7923 *
7924 * @since 2.3.0
7925 * @access private
7926 *
7927 * @see wp_clear_scheduled_hook()
7928 * @global wpdb $wpdb WordPress database abstraction object.
7929 *
7930 * @param string $new_status New post status.
7931 * @param string $old_status Previous post status.
7932 * @param WP_Post $post Post object.
7933 */
7934function _transition_post_status( $new_status, $old_status, $post ) {
7935 global $wpdb;
7936
7937 if ( 'publish' !== $old_status && 'publish' === $new_status ) {
7938 // Reset GUID if transitioning to publish and it is empty.
7939 if ( '' === get_the_guid( $post->ID ) ) {
7940 $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post->ID ) ), array( 'ID' => $post->ID ) );
7941 }
7942
7943 /**
7944 * Fires when a post's status is transitioned from private to published.
7945 *
7946 * @since 1.5.0
7947 * @deprecated 2.3.0 Use {@see 'private_to_publish'} instead.
7948 *
7949 * @param int $post_id Post ID.
7950 */
7951 do_action_deprecated( 'private_to_published', array( $post->ID ), '2.3.0', 'private_to_publish' );
7952 }
7953
7954 // If published posts changed clear the lastpostmodified cache.
7955 if ( 'publish' === $new_status || 'publish' === $old_status ) {
7956 foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) {
7957 wp_cache_delete( "lastpostmodified:$timezone", 'timeinfo' );
7958 wp_cache_delete( "lastpostdate:$timezone", 'timeinfo' );
7959 wp_cache_delete( "lastpostdate:$timezone:{$post->post_type}", 'timeinfo' );
7960 }
7961 }
7962
7963 if ( $new_status !== $old_status ) {
7964 wp_cache_delete( _count_posts_cache_key( $post->post_type ), 'counts' );
7965 wp_cache_delete( _count_posts_cache_key( $post->post_type, 'readable' ), 'counts' );
7966 }
7967
7968 // Always clears the hook in case the post status bounced from future to draft.
7969 wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) );
7970}
7971
7972/**
7973 * Hook used to schedule publication for a post marked for the future.
7974 *
7975 * The $post properties used and must exist are 'ID' and 'post_date_gmt'.
7976 *
7977 * @since 2.3.0
7978 * @access private
7979 *
7980 * @param int $deprecated Not used. Can be set to null. Never implemented. Not marked
7981 * as deprecated with _deprecated_argument() as it conflicts with
7982 * wp_transition_post_status() and the default filter for _future_post_hook().
7983 * @param WP_Post $post Post object.
7984 */
7985function _future_post_hook( $deprecated, $post ) {
7986 wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) );
7987 wp_schedule_single_event( strtotime( get_gmt_from_date( $post->post_date ) . ' GMT' ), 'publish_future_post', array( $post->ID ) );
7988}
7989
7990/**
7991 * Hook to schedule pings and enclosures when a post is published.
7992 *
7993 * Uses XMLRPC_REQUEST and WP_IMPORTING constants.
7994 *
7995 * @since 2.3.0
7996 * @access private
7997 *
7998 * @param int $post_id The ID of the post being published.
7999 */
8000function _publish_post_hook( $post_id ) {
8001 if ( defined( 'XMLRPC_REQUEST' ) ) {
8002 /**
8003 * Fires when _publish_post_hook() is called during an XML-RPC request.
8004 *
8005 * @since 2.1.0
8006 *
8007 * @param int $post_id Post ID.
8008 */
8009 do_action( 'xmlrpc_publish_post', $post_id );
8010 }
8011
8012 if ( defined( 'WP_IMPORTING' ) ) {
8013 return;
8014 }
8015
8016 if ( get_option( 'default_pingback_flag' ) ) {
8017 add_post_meta( $post_id, '_pingme', '1', true );
8018 }
8019 add_post_meta( $post_id, '_encloseme', '1', true );
8020
8021 $to_ping = get_to_ping( $post_id );
8022 if ( ! empty( $to_ping ) ) {
8023 add_post_meta( $post_id, '_trackbackme', '1' );
8024 }
8025
8026 if ( ! wp_next_scheduled( 'do_pings' ) ) {
8027 wp_schedule_single_event( time(), 'do_pings' );
8028 }
8029}
8030
8031/**
8032 * Returns the ID of the post's parent.
8033 *
8034 * @since 3.1.0
8035 * @since 5.9.0 The `$post` parameter was made optional.
8036 *
8037 * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
8038 * @return int|false Post parent ID (which can be 0 if there is no parent),
8039 * or false if the post does not exist.
8040 */
8041function wp_get_post_parent_id( $post = null ) {
8042 $post = get_post( $post );
8043
8044 if ( ! $post || is_wp_error( $post ) ) {
8045 return false;
8046 }
8047
8048 return (int) $post->post_parent;
8049}
8050
8051/**
8052 * Checks the given subset of the post hierarchy for hierarchy loops.
8053 *
8054 * Prevents loops from forming and breaks those that it finds. Attached
8055 * to the {@see 'wp_insert_post_parent'} filter.
8056 *
8057 * @since 3.1.0
8058 *
8059 * @see wp_find_hierarchy_loop()
8060 *
8061 * @param int $post_parent ID of the parent for the post we're checking.
8062 * @param int $post_id ID of the post we're checking.
8063 * @return int The new post_parent for the post, 0 otherwise.
8064 */
8065function wp_check_post_hierarchy_for_loops( $post_parent, $post_id ) {
8066 // Nothing fancy here - bail.
8067 if ( ! $post_parent ) {
8068 return 0;
8069 }
8070
8071 // New post can't cause a loop.
8072 if ( ! $post_id ) {
8073 return $post_parent;
8074 }
8075
8076 // Can't be its own parent.
8077 if ( $post_parent === $post_id ) {
8078 return 0;
8079 }
8080
8081 // Now look for larger loops.
8082 $loop = wp_find_hierarchy_loop( 'wp_get_post_parent_id', $post_id, $post_parent );
8083 if ( ! $loop ) {
8084 return $post_parent; // No loop.
8085 }
8086
8087 // Setting $post_parent to the given value causes a loop.
8088 if ( isset( $loop[ $post_id ] ) ) {
8089 return 0;
8090 }
8091
8092 // There's a loop, but it doesn't contain $post_id. Break the loop.
8093 foreach ( array_keys( $loop ) as $loop_member ) {
8094 wp_update_post(
8095 array(
8096 'ID' => $loop_member,
8097 'post_parent' => 0,
8098 )
8099 );
8100 }
8101
8102 return $post_parent;
8103}
8104
8105/**
8106 * Sets the post thumbnail (featured image) for the given post.
8107 *
8108 * @since 3.1.0
8109 *
8110 * @param int|WP_Post $post Post ID or post object where thumbnail should be attached.
8111 * @param int $thumbnail_id Thumbnail to attach.
8112 * @return int|bool Post meta ID if the key didn't exist (ie. this is the first time that
8113 * a thumbnail has been saved for the post), true on successful update,
8114 * false on failure or if the value passed is the same as the one that
8115 * is already in the database.
8116 */
8117function set_post_thumbnail( $post, $thumbnail_id ) {
8118 $post = get_post( $post );
8119 $thumbnail_id = absint( $thumbnail_id );
8120 if ( $post && $thumbnail_id && get_post( $thumbnail_id ) ) {
8121 if ( wp_get_attachment_image( $thumbnail_id, 'thumbnail' ) ) {
8122 return update_post_meta( $post->ID, '_thumbnail_id', $thumbnail_id );
8123 } else {
8124 return delete_post_meta( $post->ID, '_thumbnail_id' );
8125 }
8126 }
8127 return false;
8128}
8129
8130/**
8131 * Removes the thumbnail (featured image) from the given post.
8132 *
8133 * @since 3.3.0
8134 *
8135 * @param int|WP_Post $post Post ID or post object from which the thumbnail should be removed.
8136 * @return bool True on success, false on failure.
8137 */
8138function delete_post_thumbnail( $post ) {
8139 $post = get_post( $post );
8140 if ( $post ) {
8141 return delete_post_meta( $post->ID, '_thumbnail_id' );
8142 }
8143 return false;
8144}
8145
8146/**
8147 * Deletes auto-drafts for new posts that are > 7 days old.
8148 *
8149 * @since 3.4.0
8150 *
8151 * @global wpdb $wpdb WordPress database abstraction object.
8152 */
8153function wp_delete_auto_drafts() {
8154 global $wpdb;
8155
8156 // Cleanup old auto-drafts more than 7 days old.
8157 $old_posts = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft' AND DATE_SUB( NOW(), INTERVAL 7 DAY ) > post_date" );
8158 foreach ( (array) $old_posts as $delete ) {
8159 // Force delete.
8160 wp_delete_post( $delete, true );
8161 }
8162}
8163
8164/**
8165 * Queues posts for lazy-loading of term meta.
8166 *
8167 * @since 4.5.0
8168 *
8169 * @param WP_Post[] $posts Array of WP_Post objects.
8170 */
8171function wp_queue_posts_for_term_meta_lazyload( $posts ) {
8172 $post_type_taxonomies = array();
8173 $prime_post_terms = array();
8174 foreach ( $posts as $post ) {
8175 if ( ! ( $post instanceof WP_Post ) ) {
8176 continue;
8177 }
8178
8179 if ( ! isset( $post_type_taxonomies[ $post->post_type ] ) ) {
8180 $post_type_taxonomies[ $post->post_type ] = get_object_taxonomies( $post->post_type );
8181 }
8182
8183 foreach ( $post_type_taxonomies[ $post->post_type ] as $taxonomy ) {
8184 $prime_post_terms[ $taxonomy ][] = $post->ID;
8185 }
8186 }
8187
8188 $term_ids = array();
8189 if ( $prime_post_terms ) {
8190 foreach ( $prime_post_terms as $taxonomy => $post_ids ) {
8191 $cached_term_ids = wp_cache_get_multiple( $post_ids, "{$taxonomy}_relationships" );
8192 if ( is_array( $cached_term_ids ) ) {
8193 $cached_term_ids = array_filter( $cached_term_ids );
8194 foreach ( $cached_term_ids as $_term_ids ) {
8195 // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs.
8196 foreach ( $_term_ids as $term_id ) {
8197 if ( is_numeric( $term_id ) ) {
8198 $term_ids[] = (int) $term_id;
8199 } elseif ( isset( $term_id->term_id ) ) {
8200 $term_ids[] = (int) $term_id->term_id;
8201 }
8202 }
8203 }
8204 }
8205 }
8206 $term_ids = array_unique( $term_ids );
8207 }
8208
8209 wp_lazyload_term_meta( $term_ids );
8210}
8211
8212/**
8213 * Updates the custom taxonomies' term counts when a post's status is changed.
8214 *
8215 * For example, default posts term counts (for custom taxonomies) don't include
8216 * private / draft posts.
8217 *
8218 * @since 3.3.0
8219 * @access private
8220 *
8221 * @param string $new_status New post status.
8222 * @param string $old_status Old post status.
8223 * @param WP_Post $post Post object.
8224 */
8225function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) {
8226 if ( $new_status === $old_status ) {
8227 return;
8228 }
8229
8230 // Update counts for the post's terms.
8231 foreach ( (array) get_object_taxonomies( $post->post_type, 'objects' ) as $taxonomy ) {
8232 /** This filter is documented in wp-includes/taxonomy.php */
8233 $counted_statuses = apply_filters( 'update_post_term_count_statuses', array( 'publish' ), $taxonomy );
8234
8235 /*
8236 * Do not recalculate term count if both the old and new status are not included in term counts.
8237 * This accounts for a transition such as draft -> pending.
8238 */
8239 if ( ! in_array( $old_status, $counted_statuses, true ) && ! in_array( $new_status, $counted_statuses, true ) ) {
8240 continue;
8241 }
8242
8243 /*
8244 * Do not recalculate term count if both the old and new status are included in term counts.
8245 *
8246 * This accounts for transitioning between statuses which are both included in term counts. This can only occur
8247 * if the `update_post_term_count_statuses` filter is in use to count more than just the 'publish' status.
8248 */
8249 if ( in_array( $old_status, $counted_statuses, true ) && in_array( $new_status, $counted_statuses, true ) ) {
8250 continue;
8251 }
8252
8253 $tt_ids = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'tt_ids' ) );
8254 wp_update_term_count( $tt_ids, $taxonomy->name );
8255 }
8256}
8257
8258/**
8259 * Adds any posts from the given IDs to the cache that do not already exist in cache.
8260 *
8261 * @since 3.4.0
8262 * @since 6.1.0 This function is no longer marked as "private".
8263 *
8264 * @see update_post_cache()
8265 * @see update_postmeta_cache()
8266 * @see update_object_term_cache()
8267 *
8268 * @global wpdb $wpdb WordPress database abstraction object.
8269 *
8270 * @param int[] $ids ID list.
8271 * @param bool $update_term_cache Optional. Whether to update the term cache. Default true.
8272 * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true.
8273 */
8274function _prime_post_caches( $ids, $update_term_cache = true, $update_meta_cache = true ) {
8275 global $wpdb;
8276
8277 $non_cached_ids = _get_non_cached_ids( $ids, 'posts' );
8278 if ( ! empty( $non_cached_ids ) ) {
8279 $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) );
8280 $fresh_posts = apply_filters('godaddy/prime_post_caches/posts', $fresh_posts, $non_cached_ids);
8281
8282 if ( $fresh_posts ) {
8283 // Despite the name, update_post_cache() expects an array rather than a single post.
8284 update_post_cache( $fresh_posts );
8285 }
8286 }
8287
8288 if ( $update_meta_cache ) {
8289 update_postmeta_cache( $ids );
8290 }
8291
8292 if ( $update_term_cache ) {
8293 $post_types = array_map( 'get_post_type', $ids );
8294 $post_types = array_unique( $post_types );
8295 update_object_term_cache( $ids, $post_types );
8296 }
8297}
8298
8299/**
8300 * Prime the cache containing the parent ID of various post objects.
8301 *
8302 * @since 6.4.0
8303 *
8304 * @global wpdb $wpdb WordPress database abstraction object.
8305 *
8306 * @param int[] $ids ID list.
8307 */
8308function _prime_post_parent_id_caches( array $ids ) {
8309 global $wpdb;
8310
8311 $ids = array_filter( $ids, '_validate_cache_id' );
8312 $ids = array_unique( array_map( 'intval', $ids ), SORT_NUMERIC );
8313
8314 if ( empty( $ids ) ) {
8315 return;
8316 }
8317
8318 $cache_keys = array();
8319 foreach ( $ids as $id ) {
8320 $cache_keys[ $id ] = 'post_parent:' . (string) $id;
8321 }
8322
8323 $cached_data = wp_cache_get_multiple( array_values( $cache_keys ), 'posts' );
8324
8325 $non_cached_ids = array();
8326 foreach ( $cache_keys as $id => $cache_key ) {
8327 if ( false === $cached_data[ $cache_key ] ) {
8328 $non_cached_ids[] = $id;
8329 }
8330 }
8331
8332 if ( ! empty( $non_cached_ids ) ) {
8333 $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.ID, $wpdb->posts.post_parent FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) );
8334
8335 if ( $fresh_posts ) {
8336 $post_parent_data = array();
8337 foreach ( $fresh_posts as $fresh_post ) {
8338 $post_parent_data[ 'post_parent:' . (string) $fresh_post->ID ] = (int) $fresh_post->post_parent;
8339 }
8340
8341 wp_cache_add_multiple( $post_parent_data, 'posts' );
8342 }
8343 }
8344}
8345
8346/**
8347 * Adds a suffix if any trashed posts have a given slug.
8348 *
8349 * Store its desired (i.e. current) slug so it can try to reclaim it
8350 * if the post is untrashed.
8351 *
8352 * For internal use.
8353 *
8354 * @since 4.5.0
8355 * @access private
8356 *
8357 * @param string $post_name Post slug.
8358 * @param int $post_id Optional. Post ID that should be ignored. Default 0.
8359 */
8360function wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id = 0 ) {
8361 $trashed_posts_with_desired_slug = get_posts(
8362 array(
8363 'name' => $post_name,
8364 'post_status' => 'trash',
8365 'post_type' => 'any',
8366 'nopaging' => true,
8367 'post__not_in' => array( $post_id ),
8368 )
8369 );
8370
8371 if ( ! empty( $trashed_posts_with_desired_slug ) ) {
8372 foreach ( $trashed_posts_with_desired_slug as $_post ) {
8373 wp_add_trashed_suffix_to_post_name_for_post( $_post );
8374 }
8375 }
8376}
8377
8378/**
8379 * Adds a trashed suffix for a given post.
8380 *
8381 * Store its desired (i.e. current) slug so it can try to reclaim it
8382 * if the post is untrashed.
8383 *
8384 * For internal use.
8385 *
8386 * @since 4.5.0
8387 * @access private
8388 *
8389 * @global wpdb $wpdb WordPress database abstraction object.
8390 *
8391 * @param WP_Post $post The post.
8392 * @return string New slug for the post.
8393 */
8394function wp_add_trashed_suffix_to_post_name_for_post( $post ) {
8395 global $wpdb;
8396
8397 $post = get_post( $post );
8398
8399 if ( str_ends_with( $post->post_name, '__trashed' ) ) {
8400 return $post->post_name;
8401 }
8402 add_post_meta( $post->ID, '_wp_desired_post_slug', $post->post_name );
8403 $post_name = _truncate_post_slug( $post->post_name, 191 ) . '__trashed';
8404 $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
8405 clean_post_cache( $post->ID );
8406 return $post_name;
8407}
8408
8409/**
8410 * Sets the last changed time for the 'posts' cache group.
8411 *
8412 * @since 5.0.0
8413 */
8414function wp_cache_set_posts_last_changed() {
8415 wp_cache_set_last_changed( 'posts' );
8416}
8417
8418/**
8419 * Gets all available post MIME types for a given post type.
8420 *
8421 * @since 2.5.0
8422 *
8423 * @global wpdb $wpdb WordPress database abstraction object.
8424 *
8425 * @param string $type
8426 * @return string[] An array of MIME types.
8427 */
8428function get_available_post_mime_types( $type = 'attachment' ) {
8429 global $wpdb;
8430
8431 /**
8432 * Filters the list of available post MIME types for the given post type.
8433 *
8434 * @since 6.4.0
8435 *
8436 * @param string[]|null $mime_types An array of MIME types. Default null.
8437 * @param string $type The post type name. Usually 'attachment' but can be any post type.
8438 */
8439 $mime_types = apply_filters( 'pre_get_available_post_mime_types', null, $type );
8440
8441 if ( ! is_array( $mime_types ) ) {
8442 $mime_types = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_mime_type FROM $wpdb->posts WHERE post_type = %s AND post_mime_type != ''", $type ) );
8443 }
8444
8445 // Remove nulls from returned $mime_types.
8446 return array_values( array_filter( $mime_types ) );
8447}
8448
8449/**
8450 * Retrieves the path to an uploaded image file.
8451 *
8452 * Similar to `get_attached_file()` however some images may have been processed after uploading
8453 * to make them suitable for web use. In this case the attached "full" size file is usually replaced
8454 * with a scaled down version of the original image. This function always returns the path
8455 * to the originally uploaded image file.
8456 *
8457 * @since 5.3.0
8458 * @since 5.4.0 Added the `$unfiltered` parameter.
8459 *
8460 * @param int $attachment_id Attachment ID.
8461 * @param bool $unfiltered Optional. Passed through to `get_attached_file()`. Default false.
8462 * @return string|false Path to the original image file or false if the attachment is not an image.
8463 */
8464function wp_get_original_image_path( $attachment_id, $unfiltered = false ) {
8465 if ( ! wp_attachment_is_image( $attachment_id ) ) {
8466 return false;
8467 }
8468
8469 $image_meta = wp_get_attachment_metadata( $attachment_id );
8470 $image_file = get_attached_file( $attachment_id, $unfiltered );
8471
8472 if ( empty( $image_meta['original_image'] ) ) {
8473 $original_image = $image_file;
8474 } else {
8475 $original_image = path_join( dirname( $image_file ), $image_meta['original_image'] );
8476 }
8477
8478 /**
8479 * Filters the path to the original image.
8480 *
8481 * @since 5.3.0
8482 *
8483 * @param string $original_image Path to original image file.
8484 * @param int $attachment_id Attachment ID.
8485 */
8486 return apply_filters( 'wp_get_original_image_path', $original_image, $attachment_id );
8487}
8488
8489/**
8490 * Retrieves the URL to an original attachment image.
8491 *
8492 * Similar to `wp_get_attachment_url()` however some images may have been
8493 * processed after uploading. In this case this function returns the URL
8494 * to the originally uploaded image file.
8495 *
8496 * @since 5.3.0
8497 *
8498 * @param int $attachment_id Attachment post ID.
8499 * @return string|false Attachment image URL, false on error or if the attachment is not an image.
8500 */
8501function wp_get_original_image_url( $attachment_id ) {
8502 if ( ! wp_attachment_is_image( $attachment_id ) ) {
8503 return false;
8504 }
8505
8506 $image_url = wp_get_attachment_url( $attachment_id );
8507
8508 if ( ! $image_url ) {
8509 return false;
8510 }
8511
8512 $image_meta = wp_get_attachment_metadata( $attachment_id );
8513
8514 if ( empty( $image_meta['original_image'] ) ) {
8515 $original_image_url = $image_url;
8516 } else {
8517 $original_image_url = path_join( dirname( $image_url ), $image_meta['original_image'] );
8518 }
8519
8520 /**
8521 * Filters the URL to the original attachment image.
8522 *
8523 * @since 5.3.0
8524 *
8525 * @param string $original_image_url URL to original image.
8526 * @param int $attachment_id Attachment ID.
8527 */
8528 return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id );
8529}
8530
8531/**
8532 * Filters callback which sets the status of an untrashed post to its previous status.
8533 *
8534 * This can be used as a callback on the `wp_untrash_post_status` filter.
8535 *
8536 * @since 5.6.0
8537 *
8538 * @param string $new_status The new status of the post being restored.
8539 * @param int $post_id The ID of the post being restored.
8540 * @param string $previous_status The status of the post at the point where it was trashed.
8541 * @return string The new status of the post.
8542 */
8543function wp_untrash_post_set_previous_status( $new_status, $post_id, $previous_status ) {
8544 return $previous_status;
8545}
8546
8547/**
8548 * Returns whether the post can be edited in the block editor.
8549 *
8550 * @since 5.0.0
8551 * @since 6.1.0 Moved to wp-includes from wp-admin.
8552 *
8553 * @param int|WP_Post $post Post ID or WP_Post object.
8554 * @return bool Whether the post can be edited in the block editor.
8555 */
8556function use_block_editor_for_post( $post ) {
8557 $post = get_post( $post );
8558
8559 if ( ! $post ) {
8560 return false;
8561 }
8562
8563 // We're in the meta box loader, so don't use the block editor.
8564 if ( is_admin() && isset( $_GET['meta-box-loader'] ) ) {
8565 check_admin_referer( 'meta-box-loader', 'meta-box-loader-nonce' );
8566 return false;
8567 }
8568
8569 $use_block_editor = use_block_editor_for_post_type( $post->post_type );
8570
8571 /**
8572 * Filters whether a post is able to be edited in the block editor.
8573 *
8574 * @since 5.0.0
8575 *
8576 * @param bool $use_block_editor Whether the post can be edited or not.
8577 * @param WP_Post $post The post being checked.
8578 */
8579 return apply_filters( 'use_block_editor_for_post', $use_block_editor, $post );
8580}
8581
8582/**
8583 * Returns whether a post type is compatible with the block editor.
8584 *
8585 * The block editor depends on the REST API, and if the post type is not shown in the
8586 * REST API, then it won't work with the block editor.
8587 *
8588 * @since 5.0.0
8589 * @since 6.1.0 Moved to wp-includes from wp-admin.
8590 *
8591 * @param string $post_type The post type.
8592 * @return bool Whether the post type can be edited with the block editor.
8593 */
8594function use_block_editor_for_post_type( $post_type ) {
8595 if ( ! post_type_exists( $post_type ) ) {
8596 return false;
8597 }
8598
8599 if ( ! post_type_supports( $post_type, 'editor' ) ) {
8600 return false;
8601 }
8602
8603 $post_type_object = get_post_type_object( $post_type );
8604 if ( $post_type_object && ! $post_type_object->show_in_rest ) {
8605 return false;
8606 }
8607
8608 /**
8609 * Filters whether a post is able to be edited in the block editor.
8610 *
8611 * @since 5.0.0
8612 *
8613 * @param bool $use_block_editor Whether the post type can be edited or not. Default true.
8614 * @param string $post_type The post type being checked.
8615 */
8616 return apply_filters( 'use_block_editor_for_post_type', true, $post_type );
8617}
8618
8619/**
8620 * Registers any additional post meta fields.
8621 *
8622 * @since 6.3.0 Adds `wp_pattern_sync_status` meta field to the wp_block post type so an unsynced option can be added.
8623 *
8624 * @link https://github.com/WordPress/gutenberg/pull/51144
8625 */
8626function wp_create_initial_post_meta() {
8627 register_post_meta(
8628 'wp_block',
8629 'wp_pattern_sync_status',
8630 array(
8631 'sanitize_callback' => 'sanitize_text_field',
8632 'single' => true,
8633 'type' => 'string',
8634 'show_in_rest' => array(
8635 'schema' => array(
8636 'type' => 'string',
8637 'enum' => array( 'partial', 'unsynced' ),
8638 ),
8639 ),
8640 )
8641 );
8642}
8643