1<?php
2/**
3 * Administration API: Core Ajax handlers
4 *
5 * @package WordPress
6 * @subpackage Administration
7 * @since 2.1.0
8 */
9
10//
11// No-privilege Ajax handlers.
12//
13
14/**
15 * Ajax handler for the Heartbeat API in the no-privilege context.
16 *
17 * Runs when the user is not logged in.
18 *
19 * @since 3.6.0
20 */
21function wp_ajax_nopriv_heartbeat() {
22	$response = array();
23
24	// 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
25	if ( ! empty( $_POST['screen_id'] ) ) {
26		$screen_id = sanitize_key( $_POST['screen_id'] );
27	} else {
28		$screen_id = 'front';
29	}
30
31	if ( ! empty( $_POST['data'] ) ) {
32		$data = wp_unslash( (array) $_POST['data'] );
33
34		/**
35		 * Filters Heartbeat Ajax response in no-privilege environments.
36		 *
37		 * @since 3.6.0
38		 *
39		 * @param array  $response  The no-priv Heartbeat response.
40		 * @param array  $data      The $_POST data sent.
41		 * @param string $screen_id The screen ID.
42		 */
43		$response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id );
44	}
45
46	/**
47	 * Filters Heartbeat Ajax response in no-privilege environments when no data is passed.
48	 *
49	 * @since 3.6.0
50	 *
51	 * @param array  $response  The no-priv Heartbeat response.
52	 * @param string $screen_id The screen ID.
53	 */
54	$response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id );
55
56	/**
57	 * Fires when Heartbeat ticks in no-privilege environments.
58	 *
59	 * Allows the transport to be easily replaced with long-polling.
60	 *
61	 * @since 3.6.0
62	 *
63	 * @param array  $response  The no-priv Heartbeat response.
64	 * @param string $screen_id The screen ID.
65	 */
66	do_action( 'heartbeat_nopriv_tick', $response, $screen_id );
67
68	// Send the current time according to the server.
69	$response['server_time'] = time();
70
71	wp_send_json( $response );
72}
73
74//
75// GET-based Ajax handlers.
76//
77
78/**
79 * Ajax handler for fetching a list table.
80 *
81 * @since 3.1.0
82 */
83function wp_ajax_fetch_list() {
84	$list_class = $_GET['list_args']['class'];
85	check_ajax_referer( "fetch-list-$list_class", '_ajax_fetch_list_nonce' );
86
87	$wp_list_table = _get_list_table( $list_class, array( 'screen' => $_GET['list_args']['screen']['id'] ) );
88	if ( ! $wp_list_table ) {
89		wp_die( 0 );
90	}
91
92	if ( ! $wp_list_table->ajax_user_can() ) {
93		wp_die( -1 );
94	}
95
96	$wp_list_table->ajax_response();
97
98	wp_die( 0 );
99}
100
101/**
102 * Ajax handler for tag search.
103 *
104 * @since 3.1.0
105 */
106function wp_ajax_ajax_tag_search() {
107	if ( ! isset( $_GET['tax'] ) ) {
108		wp_die( 0 );
109	}
110
111	$taxonomy = sanitize_key( $_GET['tax'] );
112	$tax      = get_taxonomy( $taxonomy );
113
114	if ( ! $tax ) {
115		wp_die( 0 );
116	}
117
118	if ( ! current_user_can( $tax->cap->assign_terms ) ) {
119		wp_die( -1 );
120	}
121
122	$s = wp_unslash( $_GET['q'] );
123
124	$comma = _x( ',', 'tag delimiter' );
125	if ( ',' !== $comma ) {
126		$s = str_replace( $comma, ',', $s );
127	}
128
129	if ( false !== strpos( $s, ',' ) ) {
130		$s = explode( ',', $s );
131		$s = $s[ count( $s ) - 1 ];
132	}
133
134	$s = trim( $s );
135
136	/**
137	 * Filters the minimum number of characters required to fire a tag search via Ajax.
138	 *
139	 * @since 4.0.0
140	 *
141	 * @param int         $characters The minimum number of characters required. Default 2.
142	 * @param WP_Taxonomy $tax        The taxonomy object.
143	 * @param string      $s          The search term.
144	 */
145	$term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $tax, $s );
146
147	/*
148	 * Require $term_search_min_chars chars for matching (default: 2)
149	 * ensure it's a non-negative, non-zero integer.
150	 */
151	if ( ( 0 == $term_search_min_chars ) || ( strlen( $s ) < $term_search_min_chars ) ) {
152		wp_die();
153	}
154
155	$results = get_terms(
156		array(
157			'taxonomy'   => $taxonomy,
158			'name__like' => $s,
159			'fields'     => 'names',
160			'hide_empty' => false,
161		)
162	);
163
164	echo implode( "\n", $results );
165	wp_die();
166}
167
168/**
169 * Ajax handler for compression testing.
170 *
171 * @since 3.1.0
172 */
173function wp_ajax_wp_compression_test() {
174	if ( ! current_user_can( 'manage_options' ) ) {
175		wp_die( -1 );
176	}
177
178	if ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ) {
179		update_site_option( 'can_compress_scripts', 0 );
180		wp_die( 0 );
181	}
182
183	if ( isset( $_GET['test'] ) ) {
184		header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
185		header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
186		header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
187		header( 'Content-Type: application/javascript; charset=UTF-8' );
188		$force_gzip = ( defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP );
189		$test_str   = '"wpCompressionTest Lorem ipsum dolor sit amet consectetuer mollis sapien urna ut a. Eu nonummy condimentum fringilla tempor pretium platea vel nibh netus Maecenas. Hac molestie amet justo quis pellentesque est ultrices interdum nibh Morbi. Cras mattis pretium Phasellus ante ipsum ipsum ut sociis Suspendisse Lorem. Ante et non molestie. Porta urna Vestibulum egestas id congue nibh eu risus gravida sit. Ac augue auctor Ut et non a elit massa id sodales. Elit eu Nulla at nibh adipiscing mattis lacus mauris at tempus. Netus nibh quis suscipit nec feugiat eget sed lorem et urna. Pellentesque lacus at ut massa consectetuer ligula ut auctor semper Pellentesque. Ut metus massa nibh quam Curabitur molestie nec mauris congue. Volutpat molestie elit justo facilisis neque ac risus Ut nascetur tristique. Vitae sit lorem tellus et quis Phasellus lacus tincidunt nunc Fusce. Pharetra wisi Suspendisse mus sagittis libero lacinia Integer consequat ac Phasellus. Et urna ac cursus tortor aliquam Aliquam amet tellus volutpat Vestibulum. Justo interdum condimentum In augue congue tellus sollicitudin Quisque quis nibh."';
190
191		if ( 1 == $_GET['test'] ) {
192			echo $test_str;
193			wp_die();
194		} elseif ( 2 == $_GET['test'] ) {
195			if ( ! isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
196				wp_die( -1 );
197			}
198
199			if ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate' ) && function_exists( 'gzdeflate' ) && ! $force_gzip ) {
200				header( 'Content-Encoding: deflate' );
201				$out = gzdeflate( $test_str, 1 );
202			} elseif ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) && function_exists( 'gzencode' ) ) {
203				header( 'Content-Encoding: gzip' );
204				$out = gzencode( $test_str, 1 );
205			} else {
206				wp_die( -1 );
207			}
208
209			echo $out;
210			wp_die();
211		} elseif ( 'no' === $_GET['test'] ) {
212			check_ajax_referer( 'update_can_compress_scripts' );
213			update_site_option( 'can_compress_scripts', 0 );
214		} elseif ( 'yes' === $_GET['test'] ) {
215			check_ajax_referer( 'update_can_compress_scripts' );
216			update_site_option( 'can_compress_scripts', 1 );
217		}
218	}
219
220	wp_die( 0 );
221}
222
223/**
224 * Ajax handler for image editor previews.
225 *
226 * @since 3.1.0
227 */
228function wp_ajax_imgedit_preview() {
229	$post_id = (int) $_GET['postid'];
230	if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
231		wp_die( -1 );
232	}
233
234	check_ajax_referer( "image_editor-$post_id" );
235
236	include_once ABSPATH . 'wp-admin/includes/image-edit.php';
237
238	if ( ! stream_preview_image( $post_id ) ) {
239		wp_die( -1 );
240	}
241
242	wp_die();
243}
244
245/**
246 * Ajax handler for oEmbed caching.
247 *
248 * @since 3.1.0
249 *
250 * @global WP_Embed $wp_embed
251 */
252function wp_ajax_oembed_cache() {
253	$GLOBALS['wp_embed']->cache_oembed( $_GET['post'] );
254	wp_die( 0 );
255}
256
257/**
258 * Ajax handler for user autocomplete.
259 *
260 * @since 3.4.0
261 */
262function wp_ajax_autocomplete_user() {
263	if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
264		wp_die( -1 );
265	}
266
267	/** This filter is documented in wp-admin/user-new.php */
268	if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
269		wp_die( -1 );
270	}
271
272	$return = array();
273
274	// Check the type of request.
275	// Current allowed values are `add` and `search`.
276	if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) {
277		$type = $_REQUEST['autocomplete_type'];
278	} else {
279		$type = 'add';
280	}
281
282	// Check the desired field for value.
283	// Current allowed values are `user_email` and `user_login`.
284	if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) {
285		$field = $_REQUEST['autocomplete_field'];
286	} else {
287		$field = 'user_login';
288	}
289
290	// Exclude current users of this blog.
291	if ( isset( $_REQUEST['site_id'] ) ) {
292		$id = absint( $_REQUEST['site_id'] );
293	} else {
294		$id = get_current_blog_id();
295	}
296
297	$include_blog_users = ( 'search' === $type ? get_users(
298		array(
299			'blog_id' => $id,
300			'fields'  => 'ID',
301		)
302	) : array() );
303
304	$exclude_blog_users = ( 'add' === $type ? get_users(
305		array(
306			'blog_id' => $id,
307			'fields'  => 'ID',
308		)
309	) : array() );
310
311	$users = get_users(
312		array(
313			'blog_id'        => false,
314			'search'         => '*' . $_REQUEST['term'] . '*',
315			'include'        => $include_blog_users,
316			'exclude'        => $exclude_blog_users,
317			'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
318		)
319	);
320
321	foreach ( $users as $user ) {
322		$return[] = array(
323			/* translators: 1: User login, 2: User email address. */
324			'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ),
325			'value' => $user->$field,
326		);
327	}
328
329	wp_die( wp_json_encode( $return ) );
330}
331
332/**
333 * Handles Ajax requests for community events
334 *
335 * @since 4.8.0
336 */
337function wp_ajax_get_community_events() {
338	require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
339
340	check_ajax_referer( 'community_events' );
341
342	$search         = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : '';
343	$timezone       = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : '';
344	$user_id        = get_current_user_id();
345	$saved_location = get_user_option( 'community-events-location', $user_id );
346	$events_client  = new WP_Community_Events( $user_id, $saved_location );
347	$events         = $events_client->get_events( $search, $timezone );
348	$ip_changed     = false;
349
350	if ( is_wp_error( $events ) ) {
351		wp_send_json_error(
352			array(
353				'error' => $events->get_error_message(),
354			)
355		);
356	} else {
357		if ( empty( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) ) {
358			$ip_changed = true;
359		} elseif ( isset( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) && $saved_location['ip'] !== $events['location']['ip'] ) {
360			$ip_changed = true;
361		}
362
363		/*
364		 * The location should only be updated when it changes. The API doesn't always return
365		 * a full location; sometimes it's missing the description or country. The location
366		 * that was saved during the initial request is known to be good and complete, though.
367		 * It should be left intact until the user explicitly changes it (either by manually
368		 * searching for a new location, or by changing their IP address).
369		 *
370		 * If the location was updated with an incomplete response from the API, then it could
371		 * break assumptions that the UI makes (e.g., that there will always be a description
372		 * that corresponds to a latitude/longitude location).
373		 *
374		 * The location is stored network-wide, so that the user doesn't have to set it on each site.
375		 */
376		if ( $ip_changed || $search ) {
377			update_user_meta( $user_id, 'community-events-location', $events['location'] );
378		}
379
380		wp_send_json_success( $events );
381	}
382}
383
384/**
385 * Ajax handler for dashboard widgets.
386 *
387 * @since 3.4.0
388 */
389function wp_ajax_dashboard_widgets() {
390	require_once ABSPATH . 'wp-admin/includes/dashboard.php';
391
392	$pagenow = $_GET['pagenow'];
393	if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) {
394		set_current_screen( $pagenow );
395	}
396
397	switch ( $_GET['widget'] ) {
398		case 'dashboard_primary':
399			wp_dashboard_primary();
400			break;
401	}
402	wp_die();
403}
404
405/**
406 * Ajax handler for Customizer preview logged-in status.
407 *
408 * @since 3.4.0
409 */
410function wp_ajax_logged_in() {
411	wp_die( 1 );
412}
413
414//
415// Ajax helpers.
416//
417
418/**
419 * Sends back current comment total and new page links if they need to be updated.
420 *
421 * Contrary to normal success Ajax response ("1"), die with time() on success.
422 *
423 * @since 2.7.0
424 * @access private
425 *
426 * @param int $comment_id
427 * @param int $delta
428 */
429function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) {
430	$total    = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0;
431	$per_page = isset( $_POST['_per_page'] ) ? (int) $_POST['_per_page'] : 0;
432	$page     = isset( $_POST['_page'] ) ? (int) $_POST['_page'] : 0;
433	$url      = isset( $_POST['_url'] ) ? esc_url_raw( $_POST['_url'] ) : '';
434
435	// JS didn't send us everything we need to know. Just die with success message.
436	if ( ! $total || ! $per_page || ! $page || ! $url ) {
437		$time           = time();
438		$comment        = get_comment( $comment_id );
439		$comment_status = '';
440		$comment_link   = '';
441
442		if ( $comment ) {
443			$comment_status = $comment->comment_approved;
444		}
445
446		if ( 1 === (int) $comment_status ) {
447			$comment_link = get_comment_link( $comment );
448		}
449
450		$counts = wp_count_comments();
451
452		$x = new WP_Ajax_Response(
453			array(
454				'what'         => 'comment',
455				// Here for completeness - not used.
456				'id'           => $comment_id,
457				'supplemental' => array(
458					'status'               => $comment_status,
459					'postId'               => $comment ? $comment->comment_post_ID : '',
460					'time'                 => $time,
461					'in_moderation'        => $counts->moderated,
462					'i18n_comments_text'   => sprintf(
463						/* translators: %s: Number of comments. */
464						_n( '%s Comment', '%s Comments', $counts->approved ),
465						number_format_i18n( $counts->approved )
466					),
467					'i18n_moderation_text' => sprintf(
468						/* translators: %s: Number of comments. */
469						_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
470						number_format_i18n( $counts->moderated )
471					),
472					'comment_link'         => $comment_link,
473				),
474			)
475		);
476		$x->send();
477	}
478
479	$total += $delta;
480	if ( $total < 0 ) {
481		$total = 0;
482	}
483
484	// Only do the expensive stuff on a page-break, and about 1 other time per page.
485	if ( 0 == $total % $per_page || 1 == mt_rand( 1, $per_page ) ) {
486		$post_id = 0;
487		// What type of comment count are we looking for?
488		$status = 'all';
489		$parsed = parse_url( $url );
490
491		if ( isset( $parsed['query'] ) ) {
492			parse_str( $parsed['query'], $query_vars );
493
494			if ( ! empty( $query_vars['comment_status'] ) ) {
495				$status = $query_vars['comment_status'];
496			}
497
498			if ( ! empty( $query_vars['p'] ) ) {
499				$post_id = (int) $query_vars['p'];
500			}
501
502			if ( ! empty( $query_vars['comment_type'] ) ) {
503				$type = $query_vars['comment_type'];
504			}
505		}
506
507		if ( empty( $type ) ) {
508			// Only use the comment count if not filtering by a comment_type.
509			$comment_count = wp_count_comments( $post_id );
510
511			// We're looking for a known type of comment count.
512			if ( isset( $comment_count->$status ) ) {
513				$total = $comment_count->$status;
514			}
515		}
516		// Else use the decremented value from above.
517	}
518
519	// The time since the last comment count.
520	$time    = time();
521	$comment = get_comment( $comment_id );
522	$counts  = wp_count_comments();
523
524	$x = new WP_Ajax_Response(
525		array(
526			'what'         => 'comment',
527			'id'           => $comment_id,
528			'supplemental' => array(
529				'status'               => $comment ? $comment->comment_approved : '',
530				'postId'               => $comment ? $comment->comment_post_ID : '',
531				/* translators: %s: Number of comments. */
532				'total_items_i18n'     => sprintf( _n( '%s item', '%s items', $total ), number_format_i18n( $total ) ),
533				'total_pages'          => ceil( $total / $per_page ),
534				'total_pages_i18n'     => number_format_i18n( ceil( $total / $per_page ) ),
535				'total'                => $total,
536				'time'                 => $time,
537				'in_moderation'        => $counts->moderated,
538				'i18n_moderation_text' => sprintf(
539					/* translators: %s: Number of comments. */
540					_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
541					number_format_i18n( $counts->moderated )
542				),
543			),
544		)
545	);
546	$x->send();
547}
548
549//
550// POST-based Ajax handlers.
551//
552
553/**
554 * Ajax handler for adding a hierarchical term.
555 *
556 * @since 3.1.0
557 * @access private
558 */
559function _wp_ajax_add_hierarchical_term() {
560	$action   = $_POST['action'];
561	$taxonomy = get_taxonomy( substr( $action, 4 ) );
562	check_ajax_referer( $action, '_ajax_nonce-add-' . $taxonomy->name );
563
564	if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
565		wp_die( -1 );
566	}
567
568	$names  = explode( ',', $_POST[ 'new' . $taxonomy->name ] );
569	$parent = isset( $_POST[ 'new' . $taxonomy->name . '_parent' ] ) ? (int) $_POST[ 'new' . $taxonomy->name . '_parent' ] : 0;
570
571	if ( 0 > $parent ) {
572		$parent = 0;
573	}
574
575	if ( 'category' === $taxonomy->name ) {
576		$post_category = isset( $_POST['post_category'] ) ? (array) $_POST['post_category'] : array();
577	} else {
578		$post_category = ( isset( $_POST['tax_input'] ) && isset( $_POST['tax_input'][ $taxonomy->name ] ) ) ? (array) $_POST['tax_input'][ $taxonomy->name ] : array();
579	}
580
581	$checked_categories = array_map( 'absint', (array) $post_category );
582	$popular_ids        = wp_popular_terms_checklist( $taxonomy->name, 0, 10, false );
583
584	foreach ( $names as $cat_name ) {
585		$cat_name          = trim( $cat_name );
586		$category_nicename = sanitize_title( $cat_name );
587
588		if ( '' === $category_nicename ) {
589			continue;
590		}
591
592		$cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) );
593
594		if ( ! $cat_id || is_wp_error( $cat_id ) ) {
595			continue;
596		} else {
597			$cat_id = $cat_id['term_id'];
598		}
599
600		$checked_categories[] = $cat_id;
601
602		if ( $parent ) { // Do these all at once in a second.
603			continue;
604		}
605
606		ob_start();
607
608		wp_terms_checklist(
609			0,
610			array(
611				'taxonomy'             => $taxonomy->name,
612				'descendants_and_self' => $cat_id,
613				'selected_cats'        => $checked_categories,
614				'popular_cats'         => $popular_ids,
615			)
616		);
617
618		$data = ob_get_clean();
619
620		$add = array(
621			'what'     => $taxonomy->name,
622			'id'       => $cat_id,
623			'data'     => str_replace( array( "\n", "\t" ), '', $data ),
624			'position' => -1,
625		);
626	}
627
628	if ( $parent ) { // Foncy - replace the parent and all its children.
629		$parent  = get_term( $parent, $taxonomy->name );
630		$term_id = $parent->term_id;
631
632		while ( $parent->parent ) { // Get the top parent.
633			$parent = get_term( $parent->parent, $taxonomy->name );
634			if ( is_wp_error( $parent ) ) {
635				break;
636			}
637			$term_id = $parent->term_id;
638		}
639
640		ob_start();
641
642		wp_terms_checklist(
643			0,
644			array(
645				'taxonomy'             => $taxonomy->name,
646				'descendants_and_self' => $term_id,
647				'selected_cats'        => $checked_categories,
648				'popular_cats'         => $popular_ids,
649			)
650		);
651
652		$data = ob_get_clean();
653
654		$add = array(
655			'what'     => $taxonomy->name,
656			'id'       => $term_id,
657			'data'     => str_replace( array( "\n", "\t" ), '', $data ),
658			'position' => -1,
659		);
660	}
661
662	ob_start();
663
664	wp_dropdown_categories(
665		array(
666			'taxonomy'         => $taxonomy->name,
667			'hide_empty'       => 0,
668			'name'             => 'new' . $taxonomy->name . '_parent',
669			'orderby'          => 'name',
670			'hierarchical'     => 1,
671			'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;',
672		)
673	);
674
675	$sup = ob_get_clean();
676
677	$add['supplemental'] = array( 'newcat_parent' => $sup );
678
679	$x = new WP_Ajax_Response( $add );
680	$x->send();
681}
682
683/**
684 * Ajax handler for deleting a comment.
685 *
686 * @since 3.1.0
687 */
688function wp_ajax_delete_comment() {
689	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
690
691	$comment = get_comment( $id );
692
693	if ( ! $comment ) {
694		wp_die( time() );
695	}
696
697	if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
698		wp_die( -1 );
699	}
700
701	check_ajax_referer( "delete-comment_$id" );
702	$status = wp_get_comment_status( $comment );
703	$delta  = -1;
704
705	if ( isset( $_POST['trash'] ) && 1 == $_POST['trash'] ) {
706		if ( 'trash' === $status ) {
707			wp_die( time() );
708		}
709
710		$r = wp_trash_comment( $comment );
711	} elseif ( isset( $_POST['untrash'] ) && 1 == $_POST['untrash'] ) {
712		if ( 'trash' !== $status ) {
713			wp_die( time() );
714		}
715
716		$r = wp_untrash_comment( $comment );
717
718		// Undo trash, not in Trash.
719		if ( ! isset( $_POST['comment_status'] ) || 'trash' !== $_POST['comment_status'] ) {
720			$delta = 1;
721		}
722	} elseif ( isset( $_POST['spam'] ) && 1 == $_POST['spam'] ) {
723		if ( 'spam' === $status ) {
724			wp_die( time() );
725		}
726
727		$r = wp_spam_comment( $comment );
728	} elseif ( isset( $_POST['unspam'] ) && 1 == $_POST['unspam'] ) {
729		if ( 'spam' !== $status ) {
730			wp_die( time() );
731		}
732
733		$r = wp_unspam_comment( $comment );
734
735		// Undo spam, not in spam.
736		if ( ! isset( $_POST['comment_status'] ) || 'spam' !== $_POST['comment_status'] ) {
737			$delta = 1;
738		}
739	} elseif ( isset( $_POST['delete'] ) && 1 == $_POST['delete'] ) {
740		$r = wp_delete_comment( $comment );
741	} else {
742		wp_die( -1 );
743	}
744
745	if ( $r ) {
746		// Decide if we need to send back '1' or a more complicated response including page links and comment counts.
747		_wp_ajax_delete_comment_response( $comment->comment_ID, $delta );
748	}
749
750	wp_die( 0 );
751}
752
753/**
754 * Ajax handler for deleting a tag.
755 *
756 * @since 3.1.0
757 */
758function wp_ajax_delete_tag() {
759	$tag_id = (int) $_POST['tag_ID'];
760	check_ajax_referer( "delete-tag_$tag_id" );
761
762	if ( ! current_user_can( 'delete_term', $tag_id ) ) {
763		wp_die( -1 );
764	}
765
766	$taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
767	$tag      = get_term( $tag_id, $taxonomy );
768
769	if ( ! $tag || is_wp_error( $tag ) ) {
770		wp_die( 1 );
771	}
772
773	if ( wp_delete_term( $tag_id, $taxonomy ) ) {
774		wp_die( 1 );
775	} else {
776		wp_die( 0 );
777	}
778}
779
780/**
781 * Ajax handler for deleting a link.
782 *
783 * @since 3.1.0
784 */
785function wp_ajax_delete_link() {
786	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
787
788	check_ajax_referer( "delete-bookmark_$id" );
789
790	if ( ! current_user_can( 'manage_links' ) ) {
791		wp_die( -1 );
792	}
793
794	$link = get_bookmark( $id );
795	if ( ! $link || is_wp_error( $link ) ) {
796		wp_die( 1 );
797	}
798
799	if ( wp_delete_link( $id ) ) {
800		wp_die( 1 );
801	} else {
802		wp_die( 0 );
803	}
804}
805
806/**
807 * Ajax handler for deleting meta.
808 *
809 * @since 3.1.0
810 */
811function wp_ajax_delete_meta() {
812	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
813
814	check_ajax_referer( "delete-meta_$id" );
815	$meta = get_metadata_by_mid( 'post', $id );
816
817	if ( ! $meta ) {
818		wp_die( 1 );
819	}
820
821	if ( is_protected_meta( $meta->meta_key, 'post' ) || ! current_user_can( 'delete_post_meta', $meta->post_id, $meta->meta_key ) ) {
822		wp_die( -1 );
823	}
824
825	if ( delete_meta( $meta->meta_id ) ) {
826		wp_die( 1 );
827	}
828
829	wp_die( 0 );
830}
831
832/**
833 * Ajax handler for deleting a post.
834 *
835 * @since 3.1.0
836 *
837 * @param string $action Action to perform.
838 */
839function wp_ajax_delete_post( $action ) {
840	if ( empty( $action ) ) {
841		$action = 'delete-post';
842	}
843
844	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
845	check_ajax_referer( "{$action}_$id" );
846
847	if ( ! current_user_can( 'delete_post', $id ) ) {
848		wp_die( -1 );
849	}
850
851	if ( ! get_post( $id ) ) {
852		wp_die( 1 );
853	}
854
855	if ( wp_delete_post( $id ) ) {
856		wp_die( 1 );
857	} else {
858		wp_die( 0 );
859	}
860}
861
862/**
863 * Ajax handler for sending a post to the Trash.
864 *
865 * @since 3.1.0
866 *
867 * @param string $action Action to perform.
868 */
869function wp_ajax_trash_post( $action ) {
870	if ( empty( $action ) ) {
871		$action = 'trash-post';
872	}
873
874	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
875	check_ajax_referer( "{$action}_$id" );
876
877	if ( ! current_user_can( 'delete_post', $id ) ) {
878		wp_die( -1 );
879	}
880
881	if ( ! get_post( $id ) ) {
882		wp_die( 1 );
883	}
884
885	if ( 'trash-post' === $action ) {
886		$done = wp_trash_post( $id );
887	} else {
888		$done = wp_untrash_post( $id );
889	}
890
891	if ( $done ) {
892		wp_die( 1 );
893	}
894
895	wp_die( 0 );
896}
897
898/**
899 * Ajax handler to restore a post from the Trash.
900 *
901 * @since 3.1.0
902 *
903 * @param string $action Action to perform.
904 */
905function wp_ajax_untrash_post( $action ) {
906	if ( empty( $action ) ) {
907		$action = 'untrash-post';
908	}
909
910	wp_ajax_trash_post( $action );
911}
912
913/**
914 * Ajax handler to delete a page.
915 *
916 * @since 3.1.0
917 *
918 * @param string $action Action to perform.
919 */
920function wp_ajax_delete_page( $action ) {
921	if ( empty( $action ) ) {
922		$action = 'delete-page';
923	}
924
925	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
926	check_ajax_referer( "{$action}_$id" );
927
928	if ( ! current_user_can( 'delete_page', $id ) ) {
929		wp_die( -1 );
930	}
931
932	if ( ! get_post( $id ) ) {
933		wp_die( 1 );
934	}
935
936	if ( wp_delete_post( $id ) ) {
937		wp_die( 1 );
938	} else {
939		wp_die( 0 );
940	}
941}
942
943/**
944 * Ajax handler to dim a comment.
945 *
946 * @since 3.1.0
947 */
948function wp_ajax_dim_comment() {
949	$id      = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
950	$comment = get_comment( $id );
951
952	if ( ! $comment ) {
953		$x = new WP_Ajax_Response(
954			array(
955				'what' => 'comment',
956				'id'   => new WP_Error(
957					'invalid_comment',
958					/* translators: %d: Comment ID. */
959					sprintf( __( 'Comment %d does not exist' ), $id )
960				),
961			)
962		);
963		$x->send();
964	}
965
966	if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && ! current_user_can( 'moderate_comments' ) ) {
967		wp_die( -1 );
968	}
969
970	$current = wp_get_comment_status( $comment );
971
972	if ( isset( $_POST['new'] ) && $_POST['new'] == $current ) {
973		wp_die( time() );
974	}
975
976	check_ajax_referer( "approve-comment_$id" );
977
978	if ( in_array( $current, array( 'unapproved', 'spam' ), true ) ) {
979		$result = wp_set_comment_status( $comment, 'approve', true );
980	} else {
981		$result = wp_set_comment_status( $comment, 'hold', true );
982	}
983
984	if ( is_wp_error( $result ) ) {
985		$x = new WP_Ajax_Response(
986			array(
987				'what' => 'comment',
988				'id'   => $result,
989			)
990		);
991		$x->send();
992	}
993
994	// Decide if we need to send back '1' or a more complicated response including page links and comment counts.
995	_wp_ajax_delete_comment_response( $comment->comment_ID );
996	wp_die( 0 );
997}
998
999/**
1000 * Ajax handler for adding a link category.
1001 *
1002 * @since 3.1.0
1003 *
1004 * @param string $action Action to perform.
1005 */
1006function wp_ajax_add_link_category( $action ) {
1007	if ( empty( $action ) ) {
1008		$action = 'add-link-category';
1009	}
1010
1011	check_ajax_referer( $action );
1012	$tax = get_taxonomy( 'link_category' );
1013
1014	if ( ! current_user_can( $tax->cap->manage_terms ) ) {
1015		wp_die( -1 );
1016	}
1017
1018	$names = explode( ',', wp_unslash( $_POST['newcat'] ) );
1019	$x     = new WP_Ajax_Response();
1020
1021	foreach ( $names as $cat_name ) {
1022		$cat_name = trim( $cat_name );
1023		$slug     = sanitize_title( $cat_name );
1024
1025		if ( '' === $slug ) {
1026			continue;
1027		}
1028
1029		$cat_id = wp_insert_term( $cat_name, 'link_category' );
1030
1031		if ( ! $cat_id || is_wp_error( $cat_id ) ) {
1032			continue;
1033		} else {
1034			$cat_id = $cat_id['term_id'];
1035		}
1036
1037		$cat_name = esc_html( $cat_name );
1038
1039		$x->add(
1040			array(
1041				'what'     => 'link-category',
1042				'id'       => $cat_id,
1043				'data'     => "<li id='link-category-$cat_id'><label for='in-link-category-$cat_id' class='selectit'><input value='" . esc_attr( $cat_id ) . "' type='checkbox' checked='checked' name='link_category[]' id='in-link-category-$cat_id'/> $cat_name</label></li>",
1044				'position' => -1,
1045			)
1046		);
1047	}
1048	$x->send();
1049}
1050
1051/**
1052 * Ajax handler to add a tag.
1053 *
1054 * @since 3.1.0
1055 */
1056function wp_ajax_add_tag() {
1057	check_ajax_referer( 'add-tag', '_wpnonce_add-tag' );
1058	$taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
1059	$tax      = get_taxonomy( $taxonomy );
1060
1061	if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1062		wp_die( -1 );
1063	}
1064
1065	$x = new WP_Ajax_Response();
1066
1067	$tag = wp_insert_term( $_POST['tag-name'], $taxonomy, $_POST );
1068
1069	if ( $tag && ! is_wp_error( $tag ) ) {
1070		$tag = get_term( $tag['term_id'], $taxonomy );
1071	}
1072
1073	if ( ! $tag || is_wp_error( $tag ) ) {
1074		$message = __( 'An error has occurred. Please reload the page and try again.' );
1075
1076		if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
1077			$message = $tag->get_error_message();
1078		}
1079
1080		$x->add(
1081			array(
1082				'what' => 'taxonomy',
1083				'data' => new WP_Error( 'error', $message ),
1084			)
1085		);
1086		$x->send();
1087	}
1088
1089	$wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => $_POST['screen'] ) );
1090
1091	$level     = 0;
1092	$noparents = '';
1093
1094	if ( is_taxonomy_hierarchical( $taxonomy ) ) {
1095		$level = count( get_ancestors( $tag->term_id, $taxonomy, 'taxonomy' ) );
1096		ob_start();
1097		$wp_list_table->single_row( $tag, $level );
1098		$noparents = ob_get_clean();
1099	}
1100
1101	ob_start();
1102	$wp_list_table->single_row( $tag );
1103	$parents = ob_get_clean();
1104
1105	$x->add(
1106		array(
1107			'what'         => 'taxonomy',
1108			'supplemental' => compact( 'parents', 'noparents' ),
1109		)
1110	);
1111
1112	$x->add(
1113		array(
1114			'what'         => 'term',
1115			'position'     => $level,
1116			'supplemental' => (array) $tag,
1117		)
1118	);
1119
1120	$x->send();
1121}
1122
1123/**
1124 * Ajax handler for getting a tagcloud.
1125 *
1126 * @since 3.1.0
1127 */
1128function wp_ajax_get_tagcloud() {
1129	if ( ! isset( $_POST['tax'] ) ) {
1130		wp_die( 0 );
1131	}
1132
1133	$taxonomy = sanitize_key( $_POST['tax'] );
1134	$tax      = get_taxonomy( $taxonomy );
1135
1136	if ( ! $tax ) {
1137		wp_die( 0 );
1138	}
1139
1140	if ( ! current_user_can( $tax->cap->assign_terms ) ) {
1141		wp_die( -1 );
1142	}
1143
1144	$tags = get_terms(
1145		array(
1146			'taxonomy' => $taxonomy,
1147			'number'   => 45,
1148			'orderby'  => 'count',
1149			'order'    => 'DESC',
1150		)
1151	);
1152
1153	if ( empty( $tags ) ) {
1154		wp_die( $tax->labels->not_found );
1155	}
1156
1157	if ( is_wp_error( $tags ) ) {
1158		wp_die( $tags->get_error_message() );
1159	}
1160
1161	foreach ( $tags as $key => $tag ) {
1162		$tags[ $key ]->link = '#';
1163		$tags[ $key ]->id   = $tag->term_id;
1164	}
1165
1166	// We need raw tag names here, so don't filter the output.
1167	$return = wp_generate_tag_cloud(
1168		$tags,
1169		array(
1170			'filter' => 0,
1171			'format' => 'list',
1172		)
1173	);
1174
1175	if ( empty( $return ) ) {
1176		wp_die( 0 );
1177	}
1178
1179	echo $return;
1180	wp_die();
1181}
1182
1183/**
1184 * Ajax handler for getting comments.
1185 *
1186 * @since 3.1.0
1187 *
1188 * @global int $post_id
1189 *
1190 * @param string $action Action to perform.
1191 */
1192function wp_ajax_get_comments( $action ) {
1193	global $post_id;
1194
1195	if ( empty( $action ) ) {
1196		$action = 'get-comments';
1197	}
1198
1199	check_ajax_referer( $action );
1200
1201	if ( empty( $post_id ) && ! empty( $_REQUEST['p'] ) ) {
1202		$id = absint( $_REQUEST['p'] );
1203		if ( ! empty( $id ) ) {
1204			$post_id = $id;
1205		}
1206	}
1207
1208	if ( empty( $post_id ) ) {
1209		wp_die( -1 );
1210	}
1211
1212	$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1213
1214	if ( ! current_user_can( 'edit_post', $post_id ) ) {
1215		wp_die( -1 );
1216	}
1217
1218	$wp_list_table->prepare_items();
1219
1220	if ( ! $wp_list_table->has_items() ) {
1221		wp_die( 1 );
1222	}
1223
1224	$x = new WP_Ajax_Response();
1225
1226	ob_start();
1227	foreach ( $wp_list_table->items as $comment ) {
1228		if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && 0 === $comment->comment_approved ) {
1229			continue;
1230		}
1231		get_comment( $comment );
1232		$wp_list_table->single_row( $comment );
1233	}
1234	$comment_list_item = ob_get_clean();
1235
1236	$x->add(
1237		array(
1238			'what' => 'comments',
1239			'data' => $comment_list_item,
1240		)
1241	);
1242
1243	$x->send();
1244}
1245
1246/**
1247 * Ajax handler for replying to a comment.
1248 *
1249 * @since 3.1.0
1250 *
1251 * @param string $action Action to perform.
1252 */
1253function wp_ajax_replyto_comment( $action ) {
1254	if ( empty( $action ) ) {
1255		$action = 'replyto-comment';
1256	}
1257
1258	check_ajax_referer( $action, '_ajax_nonce-replyto-comment' );
1259
1260	$comment_post_ID = (int) $_POST['comment_post_ID'];
1261	$post            = get_post( $comment_post_ID );
1262
1263	if ( ! $post ) {
1264		wp_die( -1 );
1265	}
1266
1267	if ( ! current_user_can( 'edit_post', $comment_post_ID ) ) {
1268		wp_die( -1 );
1269	}
1270
1271	if ( empty( $post->post_status ) ) {
1272		wp_die( 1 );
1273	} elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) {
1274		wp_die( __( 'Error: You can&#8217;t reply to a comment on a draft post.' ) );
1275	}
1276
1277	$user = wp_get_current_user();
1278
1279	if ( $user->exists() ) {
1280		$user_ID              = $user->ID;
1281		$comment_author       = wp_slash( $user->display_name );
1282		$comment_author_email = wp_slash( $user->user_email );
1283		$comment_author_url   = wp_slash( $user->user_url );
1284		$comment_content      = trim( $_POST['content'] );
1285		$comment_type         = isset( $_POST['comment_type'] ) ? trim( $_POST['comment_type'] ) : 'comment';
1286
1287		if ( current_user_can( 'unfiltered_html' ) ) {
1288			if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
1289				$_POST['_wp_unfiltered_html_comment'] = '';
1290			}
1291
1292			if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) {
1293				kses_remove_filters(); // Start with a clean slate.
1294				kses_init_filters();   // Set up the filters.
1295				remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
1296				add_filter( 'pre_comment_content', 'wp_filter_kses' );
1297			}
1298		}
1299	} else {
1300		wp_die( __( 'Sorry, you must be logged in to reply to a comment.' ) );
1301	}
1302
1303	if ( '' === $comment_content ) {
1304		wp_die( __( 'Error: Please type your comment text.' ) );
1305	}
1306
1307	$comment_parent = 0;
1308
1309	if ( isset( $_POST['comment_ID'] ) ) {
1310		$comment_parent = absint( $_POST['comment_ID'] );
1311	}
1312
1313	$comment_auto_approved = false;
1314	$commentdata           = compact( 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_type', 'comment_parent', 'user_ID' );
1315
1316	// Automatically approve parent comment.
1317	if ( ! empty( $_POST['approve_parent'] ) ) {
1318		$parent = get_comment( $comment_parent );
1319
1320		if ( $parent && '0' === $parent->comment_approved && $parent->comment_post_ID == $comment_post_ID ) {
1321			if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) {
1322				wp_die( -1 );
1323			}
1324
1325			if ( wp_set_comment_status( $parent, 'approve' ) ) {
1326				$comment_auto_approved = true;
1327			}
1328		}
1329	}
1330
1331	$comment_id = wp_new_comment( $commentdata );
1332
1333	if ( is_wp_error( $comment_id ) ) {
1334		wp_die( $comment_id->get_error_message() );
1335	}
1336
1337	$comment = get_comment( $comment_id );
1338
1339	if ( ! $comment ) {
1340		wp_die( 1 );
1341	}
1342
1343	$position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
1344
1345	ob_start();
1346	if ( isset( $_REQUEST['mode'] ) && 'dashboard' === $_REQUEST['mode'] ) {
1347		require_once ABSPATH . 'wp-admin/includes/dashboard.php';
1348		_wp_dashboard_recent_comments_row( $comment );
1349	} else {
1350		if ( isset( $_REQUEST['mode'] ) && 'single' === $_REQUEST['mode'] ) {
1351			$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1352		} else {
1353			$wp_list_table = _get_list_table( 'WP_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1354		}
1355		$wp_list_table->single_row( $comment );
1356	}
1357	$comment_list_item = ob_get_clean();
1358
1359	$response = array(
1360		'what'     => 'comment',
1361		'id'       => $comment->comment_ID,
1362		'data'     => $comment_list_item,
1363		'position' => $position,
1364	);
1365
1366	$counts                   = wp_count_comments();
1367	$response['supplemental'] = array(
1368		'in_moderation'        => $counts->moderated,
1369		'i18n_comments_text'   => sprintf(
1370			/* translators: %s: Number of comments. */
1371			_n( '%s Comment', '%s Comments', $counts->approved ),
1372			number_format_i18n( $counts->approved )
1373		),
1374		'i18n_moderation_text' => sprintf(
1375			/* translators: %s: Number of comments. */
1376			_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
1377			number_format_i18n( $counts->moderated )
1378		),
1379	);
1380
1381	if ( $comment_auto_approved ) {
1382		$response['supplemental']['parent_approved'] = $parent->comment_ID;
1383		$response['supplemental']['parent_post_id']  = $parent->comment_post_ID;
1384	}
1385
1386	$x = new WP_Ajax_Response();
1387	$x->add( $response );
1388	$x->send();
1389}
1390
1391/**
1392 * Ajax handler for editing a comment.
1393 *
1394 * @since 3.1.0
1395 */
1396function wp_ajax_edit_comment() {
1397	check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );
1398
1399	$comment_id = (int) $_POST['comment_ID'];
1400
1401	if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
1402		wp_die( -1 );
1403	}
1404
1405	if ( '' === $_POST['content'] ) {
1406		wp_die( __( 'Error: Please type your comment text.' ) );
1407	}
1408
1409	if ( isset( $_POST['status'] ) ) {
1410		$_POST['comment_status'] = $_POST['status'];
1411	}
1412
1413	$updated = edit_comment();
1414	if ( is_wp_error( $updated ) ) {
1415		wp_die( $updated->get_error_message() );
1416	}
1417
1418	$position      = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
1419	$checkbox      = ( isset( $_POST['checkbox'] ) && true == $_POST['checkbox'] ) ? 1 : 0;
1420	$wp_list_table = _get_list_table( $checkbox ? 'WP_Comments_List_Table' : 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
1421
1422	$comment = get_comment( $comment_id );
1423
1424	if ( empty( $comment->comment_ID ) ) {
1425		wp_die( -1 );
1426	}
1427
1428	ob_start();
1429	$wp_list_table->single_row( $comment );
1430	$comment_list_item = ob_get_clean();
1431
1432	$x = new WP_Ajax_Response();
1433
1434	$x->add(
1435		array(
1436			'what'     => 'edit_comment',
1437			'id'       => $comment->comment_ID,
1438			'data'     => $comment_list_item,
1439			'position' => $position,
1440		)
1441	);
1442
1443	$x->send();
1444}
1445
1446/**
1447 * Ajax handler for adding a menu item.
1448 *
1449 * @since 3.1.0
1450 */
1451function wp_ajax_add_menu_item() {
1452	check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );
1453
1454	if ( ! current_user_can( 'edit_theme_options' ) ) {
1455		wp_die( -1 );
1456	}
1457
1458	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
1459
1460	// For performance reasons, we omit some object properties from the checklist.
1461	// The following is a hacky way to restore them when adding non-custom items.
1462	$menu_items_data = array();
1463
1464	foreach ( (array) $_POST['menu-item'] as $menu_item_data ) {
1465		if (
1466			! empty( $menu_item_data['menu-item-type'] ) &&
1467			'custom' !== $menu_item_data['menu-item-type'] &&
1468			! empty( $menu_item_data['menu-item-object-id'] )
1469		) {
1470			switch ( $menu_item_data['menu-item-type'] ) {
1471				case 'post_type':
1472					$_object = get_post( $menu_item_data['menu-item-object-id'] );
1473					break;
1474
1475				case 'post_type_archive':
1476					$_object = get_post_type_object( $menu_item_data['menu-item-object'] );
1477					break;
1478
1479				case 'taxonomy':
1480					$_object = get_term( $menu_item_data['menu-item-object-id'], $menu_item_data['menu-item-object'] );
1481					break;
1482			}
1483
1484			$_menu_items = array_map( 'wp_setup_nav_menu_item', array( $_object ) );
1485			$_menu_item  = reset( $_menu_items );
1486
1487			// Restore the missing menu item properties.
1488			$menu_item_data['menu-item-description'] = $_menu_item->description;
1489		}
1490
1491		$menu_items_data[] = $menu_item_data;
1492	}
1493
1494	$item_ids = wp_save_nav_menu_items( 0, $menu_items_data );
1495	if ( is_wp_error( $item_ids ) ) {
1496		wp_die( 0 );
1497	}
1498
1499	$menu_items = array();
1500
1501	foreach ( (array) $item_ids as $menu_item_id ) {
1502		$menu_obj = get_post( $menu_item_id );
1503
1504		if ( ! empty( $menu_obj->ID ) ) {
1505			$menu_obj        = wp_setup_nav_menu_item( $menu_obj );
1506			$menu_obj->title = empty( $menu_obj->title ) ? __( 'Menu Item' ) : $menu_obj->title;
1507			$menu_obj->label = $menu_obj->title; // Don't show "(pending)" in ajax-added items.
1508			$menu_items[]    = $menu_obj;
1509		}
1510	}
1511
1512	/** This filter is documented in wp-admin/includes/nav-menu.php */
1513	$walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $_POST['menu'] );
1514
1515	if ( ! class_exists( $walker_class_name ) ) {
1516		wp_die( 0 );
1517	}
1518
1519	if ( ! empty( $menu_items ) ) {
1520		$args = array(
1521			'after'       => '',
1522			'before'      => '',
1523			'link_after'  => '',
1524			'link_before' => '',
1525			'walker'      => new $walker_class_name,
1526		);
1527
1528		echo walk_nav_menu_tree( $menu_items, 0, (object) $args );
1529	}
1530
1531	wp_die();
1532}
1533
1534/**
1535 * Ajax handler for adding meta.
1536 *
1537 * @since 3.1.0
1538 */
1539function wp_ajax_add_meta() {
1540	check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' );
1541	$c    = 0;
1542	$pid  = (int) $_POST['post_id'];
1543	$post = get_post( $pid );
1544
1545	if ( isset( $_POST['metakeyselect'] ) || isset( $_POST['metakeyinput'] ) ) {
1546		if ( ! current_user_can( 'edit_post', $pid ) ) {
1547			wp_die( -1 );
1548		}
1549
1550		if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
1551			wp_die( 1 );
1552		}
1553
1554		// If the post is an autodraft, save the post as a draft and then attempt to save the meta.
1555		if ( 'auto-draft' === $post->post_status ) {
1556			$post_data                = array();
1557			$post_data['action']      = 'draft'; // Warning fix.
1558			$post_data['post_ID']     = $pid;
1559			$post_data['post_type']   = $post->post_type;
1560			$post_data['post_status'] = 'draft';
1561			$now                      = time();
1562			/* translators: 1: Post creation date, 2: Post creation time. */
1563			$post_data['post_title'] = sprintf( __( 'Draft created on %1$s at %2$s' ), gmdate( __( 'F j, Y' ), $now ), gmdate( __( 'g:i a' ), $now ) );
1564
1565			$pid = edit_post( $post_data );
1566
1567			if ( $pid ) {
1568				if ( is_wp_error( $pid ) ) {
1569					$x = new WP_Ajax_Response(
1570						array(
1571							'what' => 'meta',
1572							'data' => $pid,
1573						)
1574					);
1575					$x->send();
1576				}
1577
1578				$mid = add_meta( $pid );
1579				if ( ! $mid ) {
1580					wp_die( __( 'Please provide a custom field value.' ) );
1581				}
1582			} else {
1583				wp_die( 0 );
1584			}
1585		} else {
1586			$mid = add_meta( $pid );
1587			if ( ! $mid ) {
1588				wp_die( __( 'Please provide a custom field value.' ) );
1589			}
1590		}
1591
1592		$meta = get_metadata_by_mid( 'post', $mid );
1593		$pid  = (int) $meta->post_id;
1594		$meta = get_object_vars( $meta );
1595
1596		$x = new WP_Ajax_Response(
1597			array(
1598				'what'         => 'meta',
1599				'id'           => $mid,
1600				'data'         => _list_meta_row( $meta, $c ),
1601				'position'     => 1,
1602				'supplemental' => array( 'postid' => $pid ),
1603			)
1604		);
1605	} else { // Update?
1606		$mid   = (int) key( $_POST['meta'] );
1607		$key   = wp_unslash( $_POST['meta'][ $mid ]['key'] );
1608		$value = wp_unslash( $_POST['meta'][ $mid ]['value'] );
1609
1610		if ( '' === trim( $key ) ) {
1611			wp_die( __( 'Please provide a custom field name.' ) );
1612		}
1613
1614		$meta = get_metadata_by_mid( 'post', $mid );
1615
1616		if ( ! $meta ) {
1617			wp_die( 0 ); // If meta doesn't exist.
1618		}
1619
1620		if (
1621			is_protected_meta( $meta->meta_key, 'post' ) || is_protected_meta( $key, 'post' ) ||
1622			! current_user_can( 'edit_post_meta', $meta->post_id, $meta->meta_key ) ||
1623			! current_user_can( 'edit_post_meta', $meta->post_id, $key )
1624		) {
1625			wp_die( -1 );
1626		}
1627
1628		if ( $meta->meta_value != $value || $meta->meta_key != $key ) {
1629			$u = update_metadata_by_mid( 'post', $mid, $value, $key );
1630			if ( ! $u ) {
1631				wp_die( 0 ); // We know meta exists; we also know it's unchanged (or DB error, in which case there are bigger problems).
1632			}
1633		}
1634
1635		$x = new WP_Ajax_Response(
1636			array(
1637				'what'         => 'meta',
1638				'id'           => $mid,
1639				'old_id'       => $mid,
1640				'data'         => _list_meta_row(
1641					array(
1642						'meta_key'   => $key,
1643						'meta_value' => $value,
1644						'meta_id'    => $mid,
1645					),
1646					$c
1647				),
1648				'position'     => 0,
1649				'supplemental' => array( 'postid' => $meta->post_id ),
1650			)
1651		);
1652	}
1653	$x->send();
1654}
1655
1656/**
1657 * Ajax handler for adding a user.
1658 *
1659 * @since 3.1.0
1660 *
1661 * @param string $action Action to perform.
1662 */
1663function wp_ajax_add_user( $action ) {
1664	if ( empty( $action ) ) {
1665		$action = 'add-user';
1666	}
1667
1668	check_ajax_referer( $action );
1669
1670	if ( ! current_user_can( 'create_users' ) ) {
1671		wp_die( -1 );
1672	}
1673
1674	$user_id = edit_user();
1675
1676	if ( ! $user_id ) {
1677		wp_die( 0 );
1678	} elseif ( is_wp_error( $user_id ) ) {
1679		$x = new WP_Ajax_Response(
1680			array(
1681				'what' => 'user',
1682				'id'   => $user_id,
1683			)
1684		);
1685		$x->send();
1686	}
1687
1688	$user_object   = get_userdata( $user_id );
1689	$wp_list_table = _get_list_table( 'WP_Users_List_Table' );
1690
1691	$role = current( $user_object->roles );
1692
1693	$x = new WP_Ajax_Response(
1694		array(
1695			'what'         => 'user',
1696			'id'           => $user_id,
1697			'data'         => $wp_list_table->single_row( $user_object, '', $role ),
1698			'supplemental' => array(
1699				'show-link' => sprintf(
1700					/* translators: %s: The new user. */
1701					__( 'User %s added' ),
1702					'<a href="#user-' . $user_id . '">' . $user_object->user_login . '</a>'
1703				),
1704				'role'      => $role,
1705			),
1706		)
1707	);
1708	$x->send();
1709}
1710
1711/**
1712 * Ajax handler for closed post boxes.
1713 *
1714 * @since 3.1.0
1715 */
1716function wp_ajax_closed_postboxes() {
1717	check_ajax_referer( 'closedpostboxes', 'closedpostboxesnonce' );
1718	$closed = isset( $_POST['closed'] ) ? explode( ',', $_POST['closed'] ) : array();
1719	$closed = array_filter( $closed );
1720
1721	$hidden = isset( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
1722	$hidden = array_filter( $hidden );
1723
1724	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';
1725
1726	if ( sanitize_key( $page ) != $page ) {
1727		wp_die( 0 );
1728	}
1729
1730	$user = wp_get_current_user();
1731	if ( ! $user ) {
1732		wp_die( -1 );
1733	}
1734
1735	if ( is_array( $closed ) ) {
1736		update_user_meta( $user->ID, "closedpostboxes_$page", $closed );
1737	}
1738
1739	if ( is_array( $hidden ) ) {
1740		// Postboxes that are always shown.
1741		$hidden = array_diff( $hidden, array( 'submitdiv', 'linksubmitdiv', 'manage-menu', 'create-menu' ) );
1742		update_user_meta( $user->ID, "metaboxhidden_$page", $hidden );
1743	}
1744
1745	wp_die( 1 );
1746}
1747
1748/**
1749 * Ajax handler for hidden columns.
1750 *
1751 * @since 3.1.0
1752 */
1753function wp_ajax_hidden_columns() {
1754	check_ajax_referer( 'screen-options-nonce', 'screenoptionnonce' );
1755	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';
1756
1757	if ( sanitize_key( $page ) != $page ) {
1758		wp_die( 0 );
1759	}
1760
1761	$user = wp_get_current_user();
1762	if ( ! $user ) {
1763		wp_die( -1 );
1764	}
1765
1766	$hidden = ! empty( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
1767	update_user_meta( $user->ID, "manage{$page}columnshidden", $hidden );
1768
1769	wp_die( 1 );
1770}
1771
1772/**
1773 * Ajax handler for updating whether to display the welcome panel.
1774 *
1775 * @since 3.1.0
1776 */
1777function wp_ajax_update_welcome_panel() {
1778	check_ajax_referer( 'welcome-panel-nonce', 'welcomepanelnonce' );
1779
1780	if ( ! current_user_can( 'edit_theme_options' ) ) {
1781		wp_die( -1 );
1782	}
1783
1784	update_user_meta( get_current_user_id(), 'show_welcome_panel', empty( $_POST['visible'] ) ? 0 : 1 );
1785
1786	wp_die( 1 );
1787}
1788
1789/**
1790 * Ajax handler for retrieving menu meta boxes.
1791 *
1792 * @since 3.1.0
1793 */
1794function wp_ajax_menu_get_metabox() {
1795	if ( ! current_user_can( 'edit_theme_options' ) ) {
1796		wp_die( -1 );
1797	}
1798
1799	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
1800
1801	if ( isset( $_POST['item-type'] ) && 'post_type' === $_POST['item-type'] ) {
1802		$type     = 'posttype';
1803		$callback = 'wp_nav_menu_item_post_type_meta_box';
1804		$items    = (array) get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
1805	} elseif ( isset( $_POST['item-type'] ) && 'taxonomy' === $_POST['item-type'] ) {
1806		$type     = 'taxonomy';
1807		$callback = 'wp_nav_menu_item_taxonomy_meta_box';
1808		$items    = (array) get_taxonomies( array( 'show_ui' => true ), 'object' );
1809	}
1810
1811	if ( ! empty( $_POST['item-object'] ) && isset( $items[ $_POST['item-object'] ] ) ) {
1812		$menus_meta_box_object = $items[ $_POST['item-object'] ];
1813
1814		/** This filter is documented in wp-admin/includes/nav-menu.php */
1815		$item = apply_filters( 'nav_menu_meta_box_object', $menus_meta_box_object );
1816
1817		$box_args = array(
1818			'id'       => 'add-' . $item->name,
1819			'title'    => $item->labels->name,
1820			'callback' => $callback,
1821			'args'     => $item,
1822		);
1823
1824		ob_start();
1825		$callback( null, $box_args );
1826
1827		$markup = ob_get_clean();
1828
1829		echo wp_json_encode(
1830			array(
1831				'replace-id' => $type . '-' . $item->name,
1832				'markup'     => $markup,
1833			)
1834		);
1835	}
1836
1837	wp_die();
1838}
1839
1840/**
1841 * Ajax handler for internal linking.
1842 *
1843 * @since 3.1.0
1844 */
1845function wp_ajax_wp_link_ajax() {
1846	check_ajax_referer( 'internal-linking', '_ajax_linking_nonce' );
1847
1848	$args = array();
1849
1850	if ( isset( $_POST['search'] ) ) {
1851		$args['s'] = wp_unslash( $_POST['search'] );
1852	}
1853
1854	if ( isset( $_POST['term'] ) ) {
1855		$args['s'] = wp_unslash( $_POST['term'] );
1856	}
1857
1858	$args['pagenum'] = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
1859
1860	if ( ! class_exists( '_WP_Editors', false ) ) {
1861		require ABSPATH . WPINC . '/class-wp-editor.php';
1862	}
1863
1864	$results = _WP_Editors::wp_link_query( $args );
1865
1866	if ( ! isset( $results ) ) {
1867		wp_die( 0 );
1868	}
1869
1870	echo wp_json_encode( $results );
1871	echo "\n";
1872
1873	wp_die();
1874}
1875
1876/**
1877 * Ajax handler for menu locations save.
1878 *
1879 * @since 3.1.0
1880 */
1881function wp_ajax_menu_locations_save() {
1882	if ( ! current_user_can( 'edit_theme_options' ) ) {
1883		wp_die( -1 );
1884	}
1885
1886	check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );
1887
1888	if ( ! isset( $_POST['menu-locations'] ) ) {
1889		wp_die( 0 );
1890	}
1891
1892	set_theme_mod( 'nav_menu_locations', array_map( 'absint', $_POST['menu-locations'] ) );
1893	wp_die( 1 );
1894}
1895
1896/**
1897 * Ajax handler for saving the meta box order.
1898 *
1899 * @since 3.1.0
1900 */
1901function wp_ajax_meta_box_order() {
1902	check_ajax_referer( 'meta-box-order' );
1903	$order        = isset( $_POST['order'] ) ? (array) $_POST['order'] : false;
1904	$page_columns = isset( $_POST['page_columns'] ) ? $_POST['page_columns'] : 'auto';
1905
1906	if ( 'auto' !== $page_columns ) {
1907		$page_columns = (int) $page_columns;
1908	}
1909
1910	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';
1911
1912	if ( sanitize_key( $page ) != $page ) {
1913		wp_die( 0 );
1914	}
1915
1916	$user = wp_get_current_user();
1917	if ( ! $user ) {
1918		wp_die( -1 );
1919	}
1920
1921	if ( $order ) {
1922		update_user_meta( $user->ID, "meta-box-order_$page", $order );
1923	}
1924
1925	if ( $page_columns ) {
1926		update_user_meta( $user->ID, "screen_layout_$page", $page_columns );
1927	}
1928
1929	wp_send_json_success();
1930}
1931
1932/**
1933 * Ajax handler for menu quick searching.
1934 *
1935 * @since 3.1.0
1936 */
1937function wp_ajax_menu_quick_search() {
1938	if ( ! current_user_can( 'edit_theme_options' ) ) {
1939		wp_die( -1 );
1940	}
1941
1942	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
1943
1944	_wp_ajax_menu_quick_search( $_POST );
1945
1946	wp_die();
1947}
1948
1949/**
1950 * Ajax handler to retrieve a permalink.
1951 *
1952 * @since 3.1.0
1953 */
1954function wp_ajax_get_permalink() {
1955	check_ajax_referer( 'getpermalink', 'getpermalinknonce' );
1956	$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
1957	wp_die( get_preview_post_link( $post_id ) );
1958}
1959
1960/**
1961 * Ajax handler to retrieve a sample permalink.
1962 *
1963 * @since 3.1.0
1964 */
1965function wp_ajax_sample_permalink() {
1966	check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' );
1967	$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
1968	$title   = isset( $_POST['new_title'] ) ? $_POST['new_title'] : '';
1969	$slug    = isset( $_POST['new_slug'] ) ? $_POST['new_slug'] : null;
1970	wp_die( get_sample_permalink_html( $post_id, $title, $slug ) );
1971}
1972
1973/**
1974 * Ajax handler for Quick Edit saving a post from a list table.
1975 *
1976 * @since 3.1.0
1977 *
1978 * @global string $mode List table view mode.
1979 */
1980function wp_ajax_inline_save() {
1981	global $mode;
1982
1983	check_ajax_referer( 'inlineeditnonce', '_inline_edit' );
1984
1985	if ( ! isset( $_POST['post_ID'] ) || ! (int) $_POST['post_ID'] ) {
1986		wp_die();
1987	}
1988
1989	$post_ID = (int) $_POST['post_ID'];
1990
1991	if ( 'page' === $_POST['post_type'] ) {
1992		if ( ! current_user_can( 'edit_page', $post_ID ) ) {
1993			wp_die( __( 'Sorry, you are not allowed to edit this page.' ) );
1994		}
1995	} else {
1996		if ( ! current_user_can( 'edit_post', $post_ID ) ) {
1997			wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
1998		}
1999	}
2000
2001	$last = wp_check_post_lock( $post_ID );
2002	if ( $last ) {
2003		$last_user      = get_userdata( $last );
2004		$last_user_name = $last_user ? $last_user->display_name : __( 'Someone' );
2005
2006		/* translators: %s: User's display name. */
2007		$msg_template = __( 'Saving is disabled: %s is currently editing this post.' );
2008
2009		if ( 'page' === $_POST['post_type'] ) {
2010			/* translators: %s: User's display name. */
2011			$msg_template = __( 'Saving is disabled: %s is currently editing this page.' );
2012		}
2013
2014		printf( $msg_template, esc_html( $last_user_name ) );
2015		wp_die();
2016	}
2017
2018	$data = &$_POST;
2019
2020	$post = get_post( $post_ID, ARRAY_A );
2021
2022	// Since it's coming from the database.
2023	$post = wp_slash( $post );
2024
2025	$data['content'] = $post['post_content'];
2026	$data['excerpt'] = $post['post_excerpt'];
2027
2028	// Rename.
2029	$data['user_ID'] = get_current_user_id();
2030
2031	if ( isset( $data['post_parent'] ) ) {
2032		$data['parent_id'] = $data['post_parent'];
2033	}
2034
2035	// Status.
2036	if ( isset( $data['keep_private'] ) && 'private' === $data['keep_private'] ) {
2037		$data['visibility']  = 'private';
2038		$data['post_status'] = 'private';
2039	} else {
2040		$data['post_status'] = $data['_status'];
2041	}
2042
2043	if ( empty( $data['comment_status'] ) ) {
2044		$data['comment_status'] = 'closed';
2045	}
2046
2047	if ( empty( $data['ping_status'] ) ) {
2048		$data['ping_status'] = 'closed';
2049	}
2050
2051	// Exclude terms from taxonomies that are not supposed to appear in Quick Edit.
2052	if ( ! empty( $data['tax_input'] ) ) {
2053		foreach ( $data['tax_input'] as $taxonomy => $terms ) {
2054			$tax_object = get_taxonomy( $taxonomy );
2055			/** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */
2056			if ( ! apply_filters( 'quick_edit_show_taxonomy', $tax_object->show_in_quick_edit, $taxonomy, $post['post_type'] ) ) {
2057				unset( $data['tax_input'][ $taxonomy ] );
2058			}
2059		}
2060	}
2061
2062	// Hack: wp_unique_post_slug() doesn't work for drafts, so we will fake that our post is published.
2063	if ( ! empty( $data['post_name'] ) && in_array( $post['post_status'], array( 'draft', 'pending' ), true ) ) {
2064		$post['post_status'] = 'publish';
2065		$data['post_name']   = wp_unique_post_slug( $data['post_name'], $post['ID'], $post['post_status'], $post['post_type'], $post['post_parent'] );
2066	}
2067
2068	// Update the post.
2069	edit_post();
2070
2071	$wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => $_POST['screen'] ) );
2072
2073	$mode = 'excerpt' === $_POST['post_view'] ? 'excerpt' : 'list';
2074
2075	$level = 0;
2076	if ( is_post_type_hierarchical( $wp_list_table->screen->post_type ) ) {
2077		$request_post = array( get_post( $_POST['post_ID'] ) );
2078		$parent       = $request_post[0]->post_parent;
2079
2080		while ( $parent > 0 ) {
2081			$parent_post = get_post( $parent );
2082			$parent      = $parent_post->post_parent;
2083			$level++;
2084		}
2085	}
2086
2087	$wp_list_table->display_rows( array( get_post( $_POST['post_ID'] ) ), $level );
2088
2089	wp_die();
2090}
2091
2092/**
2093 * Ajax handler for quick edit saving for a term.
2094 *
2095 * @since 3.1.0
2096 */
2097function wp_ajax_inline_save_tax() {
2098	check_ajax_referer( 'taxinlineeditnonce', '_inline_edit' );
2099
2100	$taxonomy = sanitize_key( $_POST['taxonomy'] );
2101	$tax      = get_taxonomy( $taxonomy );
2102
2103	if ( ! $tax ) {
2104		wp_die( 0 );
2105	}
2106
2107	if ( ! isset( $_POST['tax_ID'] ) || ! (int) $_POST['tax_ID'] ) {
2108		wp_die( -1 );
2109	}
2110
2111	$id = (int) $_POST['tax_ID'];
2112
2113	if ( ! current_user_can( 'edit_term', $id ) ) {
2114		wp_die( -1 );
2115	}
2116
2117	$wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => 'edit-' . $taxonomy ) );
2118
2119	$tag                  = get_term( $id, $taxonomy );
2120	$_POST['description'] = $tag->description;
2121
2122	$updated = wp_update_term( $id, $taxonomy, $_POST );
2123
2124	if ( $updated && ! is_wp_error( $updated ) ) {
2125		$tag = get_term( $updated['term_id'], $taxonomy );
2126		if ( ! $tag || is_wp_error( $tag ) ) {
2127			if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
2128				wp_die( $tag->get_error_message() );
2129			}
2130			wp_die( __( 'Item not updated.' ) );
2131		}
2132	} else {
2133		if ( is_wp_error( $updated ) && $updated->get_error_message() ) {
2134			wp_die( $updated->get_error_message() );
2135		}
2136		wp_die( __( 'Item not updated.' ) );
2137	}
2138
2139	$level  = 0;
2140	$parent = $tag->parent;
2141
2142	while ( $parent > 0 ) {
2143		$parent_tag = get_term( $parent, $taxonomy );
2144		$parent     = $parent_tag->parent;
2145		$level++;
2146	}
2147
2148	$wp_list_table->single_row( $tag, $level );
2149	wp_die();
2150}
2151
2152/**
2153 * Ajax handler for querying posts for the Find Posts modal.
2154 *
2155 * @see window.findPosts
2156 *
2157 * @since 3.1.0
2158 */
2159function wp_ajax_find_posts() {
2160	check_ajax_referer( 'find-posts' );
2161
2162	$post_types = get_post_types( array( 'public' => true ), 'objects' );
2163	unset( $post_types['attachment'] );
2164
2165	$s    = wp_unslash( $_POST['ps'] );
2166	$args = array(
2167		'post_type'      => array_keys( $post_types ),
2168		'post_status'    => 'any',
2169		'posts_per_page' => 50,
2170	);
2171
2172	if ( '' !== $s ) {
2173		$args['s'] = $s;
2174	}
2175
2176	$posts = get_posts( $args );
2177
2178	if ( ! $posts ) {
2179		wp_send_json_error( __( 'No items found.' ) );
2180	}
2181
2182	$html = '<table class="widefat"><thead><tr><th class="found-radio"><br /></th><th>' . __( 'Title' ) . '</th><th class="no-break">' . __( 'Type' ) . '</th><th class="no-break">' . __( 'Date' ) . '</th><th class="no-break">' . __( 'Status' ) . '</th></tr></thead><tbody>';
2183	$alt  = '';
2184	foreach ( $posts as $post ) {
2185		$title = trim( $post->post_title ) ? $post->post_title : __( '(no title)' );
2186		$alt   = ( 'alternate' === $alt ) ? '' : 'alternate';
2187
2188		switch ( $post->post_status ) {
2189			case 'publish':
2190			case 'private':
2191				$stat = __( 'Published' );
2192				break;
2193			case 'future':
2194				$stat = __( 'Scheduled' );
2195				break;
2196			case 'pending':
2197				$stat = __( 'Pending Review' );
2198				break;
2199			case 'draft':
2200				$stat = __( 'Draft' );
2201				break;
2202		}
2203
2204		if ( '0000-00-00 00:00:00' === $post->post_date ) {
2205			$time = '';
2206		} else {
2207			/* translators: Date format in table columns, see https://www.php.net/manual/datetime.format.php */
2208			$time = mysql2date( __( 'Y/m/d' ), $post->post_date );
2209		}
2210
2211		$html .= '<tr class="' . trim( 'found-posts ' . $alt ) . '"><td class="found-radio"><input type="radio" id="found-' . $post->ID . '" name="found_post_id" value="' . esc_attr( $post->ID ) . '"></td>';
2212		$html .= '<td><label for="found-' . $post->ID . '">' . esc_html( $title ) . '</label></td><td class="no-break">' . esc_html( $post_types[ $post->post_type ]->labels->singular_name ) . '</td><td class="no-break">' . esc_html( $time ) . '</td><td class="no-break">' . esc_html( $stat ) . ' </td></tr>' . "\n\n";
2213	}
2214
2215	$html .= '</tbody></table>';
2216
2217	wp_send_json_success( $html );
2218}
2219
2220/**
2221 * Ajax handler for saving the widgets order.
2222 *
2223 * @since 3.1.0
2224 */
2225function wp_ajax_widgets_order() {
2226	check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );
2227
2228	if ( ! current_user_can( 'edit_theme_options' ) ) {
2229		wp_die( -1 );
2230	}
2231
2232	unset( $_POST['savewidgets'], $_POST['action'] );
2233
2234	// Save widgets order for all sidebars.
2235	if ( is_array( $_POST['sidebars'] ) ) {
2236		$sidebars = array();
2237
2238		foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) {
2239			$sb = array();
2240
2241			if ( ! empty( $val ) ) {
2242				$val = explode( ',', $val );
2243
2244				foreach ( $val as $k => $v ) {
2245					if ( strpos( $v, 'widget-' ) === false ) {
2246						continue;
2247					}
2248
2249					$sb[ $k ] = substr( $v, strpos( $v, '_' ) + 1 );
2250				}
2251			}
2252			$sidebars[ $key ] = $sb;
2253		}
2254
2255		wp_set_sidebars_widgets( $sidebars );
2256		wp_die( 1 );
2257	}
2258
2259	wp_die( -1 );
2260}
2261
2262/**
2263 * Ajax handler for saving a widget.
2264 *
2265 * @since 3.1.0
2266 *
2267 * @global array $wp_registered_widgets
2268 * @global array $wp_registered_widget_controls
2269 * @global array $wp_registered_widget_updates
2270 */
2271function wp_ajax_save_widget() {
2272	global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates;
2273
2274	check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );
2275
2276	if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['id_base'] ) ) {
2277		wp_die( -1 );
2278	}
2279
2280	unset( $_POST['savewidgets'], $_POST['action'] );
2281
2282	/**
2283	 * Fires early when editing the widgets displayed in sidebars.
2284	 *
2285	 * @since 2.8.0
2286	 */
2287	do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2288
2289	/**
2290	 * Fires early when editing the widgets displayed in sidebars.
2291	 *
2292	 * @since 2.8.0
2293	 */
2294	do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2295
2296	/** This action is documented in wp-admin/widgets.php */
2297	do_action( 'sidebar_admin_setup' );
2298
2299	$id_base      = wp_unslash( $_POST['id_base'] );
2300	$widget_id    = wp_unslash( $_POST['widget-id'] );
2301	$sidebar_id   = $_POST['sidebar'];
2302	$multi_number = ! empty( $_POST['multi_number'] ) ? (int) $_POST['multi_number'] : 0;
2303	$settings     = isset( $_POST[ 'widget-' . $id_base ] ) && is_array( $_POST[ 'widget-' . $id_base ] ) ? $_POST[ 'widget-' . $id_base ] : false;
2304	$error        = '<p>' . __( 'An error has occurred. Please reload the page and try again.' ) . '</p>';
2305
2306	$sidebars = wp_get_sidebars_widgets();
2307	$sidebar  = isset( $sidebars[ $sidebar_id ] ) ? $sidebars[ $sidebar_id ] : array();
2308
2309	// Delete.
2310	if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {
2311
2312		if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
2313			wp_die( $error );
2314		}
2315
2316		$sidebar = array_diff( $sidebar, array( $widget_id ) );
2317		$_POST   = array(
2318			'sidebar'            => $sidebar_id,
2319			'widget-' . $id_base => array(),
2320			'the-widget-id'      => $widget_id,
2321			'delete_widget'      => '1',
2322		);
2323
2324		/** This action is documented in wp-admin/widgets.php */
2325		do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base );
2326
2327	} elseif ( $settings && preg_match( '/__i__|%i%/', key( $settings ) ) ) {
2328		if ( ! $multi_number ) {
2329			wp_die( $error );
2330		}
2331
2332		$_POST[ 'widget-' . $id_base ] = array( $multi_number => reset( $settings ) );
2333		$widget_id                     = $id_base . '-' . $multi_number;
2334		$sidebar[]                     = $widget_id;
2335	}
2336	$_POST['widget-id'] = $sidebar;
2337
2338	foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
2339
2340		if ( $name == $id_base ) {
2341			if ( ! is_callable( $control['callback'] ) ) {
2342				continue;
2343			}
2344
2345			ob_start();
2346				call_user_func_array( $control['callback'], $control['params'] );
2347			ob_end_clean();
2348			break;
2349		}
2350	}
2351
2352	if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {
2353		$sidebars[ $sidebar_id ] = $sidebar;
2354		wp_set_sidebars_widgets( $sidebars );
2355		echo "deleted:$widget_id";
2356		wp_die();
2357	}
2358
2359	if ( ! empty( $_POST['add_new'] ) ) {
2360		wp_die();
2361	}
2362
2363	$form = $wp_registered_widget_controls[ $widget_id ];
2364	if ( $form ) {
2365		call_user_func_array( $form['callback'], $form['params'] );
2366	}
2367
2368	wp_die();
2369}
2370
2371/**
2372 * Ajax handler for updating a widget.
2373 *
2374 * @since 3.9.0
2375 *
2376 * @global WP_Customize_Manager $wp_customize
2377 */
2378function wp_ajax_update_widget() {
2379	global $wp_customize;
2380	$wp_customize->widgets->wp_ajax_update_widget();
2381}
2382
2383/**
2384 * Ajax handler for removing inactive widgets.
2385 *
2386 * @since 4.4.0
2387 */
2388function wp_ajax_delete_inactive_widgets() {
2389	check_ajax_referer( 'remove-inactive-widgets', 'removeinactivewidgets' );
2390
2391	if ( ! current_user_can( 'edit_theme_options' ) ) {
2392		wp_die( -1 );
2393	}
2394
2395	unset( $_POST['removeinactivewidgets'], $_POST['action'] );
2396	/** This action is documented in wp-admin/includes/ajax-actions.php */
2397	do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2398	/** This action is documented in wp-admin/includes/ajax-actions.php */
2399	do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
2400	/** This action is documented in wp-admin/widgets.php */
2401	do_action( 'sidebar_admin_setup' );
2402
2403	$sidebars_widgets = wp_get_sidebars_widgets();
2404
2405	foreach ( $sidebars_widgets['wp_inactive_widgets'] as $key => $widget_id ) {
2406		$pieces       = explode( '-', $widget_id );
2407		$multi_number = array_pop( $pieces );
2408		$id_base      = implode( '-', $pieces );
2409		$widget       = get_option( 'widget_' . $id_base );
2410		unset( $widget[ $multi_number ] );
2411		update_option( 'widget_' . $id_base, $widget );
2412		unset( $sidebars_widgets['wp_inactive_widgets'][ $key ] );
2413	}
2414
2415	wp_set_sidebars_widgets( $sidebars_widgets );
2416
2417	wp_die();
2418}
2419
2420/**
2421 * Ajax handler for creating missing image sub-sizes for just uploaded images.
2422 *
2423 * @since 5.3.0
2424 */
2425function wp_ajax_media_create_image_subsizes() {
2426	check_ajax_referer( 'media-form' );
2427
2428	if ( ! current_user_can( 'upload_files' ) ) {
2429		wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to upload files.' ) ) );
2430	}
2431
2432	if ( empty( $_POST['attachment_id'] ) ) {
2433		wp_send_json_error( array( 'message' => __( 'Upload failed. Please reload and try again.' ) ) );
2434	}
2435
2436	$attachment_id = (int) $_POST['attachment_id'];
2437
2438	if ( ! empty( $_POST['_wp_upload_failed_cleanup'] ) ) {
2439		// Upload failed. Cleanup.
2440		if ( wp_attachment_is_image( $attachment_id ) && current_user_can( 'delete_post', $attachment_id ) ) {
2441			$attachment = get_post( $attachment_id );
2442
2443			// Created at most 10 min ago.
2444			if ( $attachment && ( time() - strtotime( $attachment->post_date_gmt ) < 600 ) ) {
2445				wp_delete_attachment( $attachment_id, true );
2446				wp_send_json_success();
2447			}
2448		}
2449	}
2450
2451	// Set a custom header with the attachment_id.
2452	// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
2453	if ( ! headers_sent() ) {
2454		header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
2455	}
2456
2457	// This can still be pretty slow and cause timeout or out of memory errors.
2458	// The js that handles the response would need to also handle HTTP 500 errors.
2459	wp_update_image_subsizes( $attachment_id );
2460
2461	if ( ! empty( $_POST['_legacy_support'] ) ) {
2462		// The old (inline) uploader. Only needs the attachment_id.
2463		$response = array( 'id' => $attachment_id );
2464	} else {
2465		// Media modal and Media Library grid view.
2466		$response = wp_prepare_attachment_for_js( $attachment_id );
2467
2468		if ( ! $response ) {
2469			wp_send_json_error( array( 'message' => __( 'Upload failed.' ) ) );
2470		}
2471	}
2472
2473	// At this point the image has been uploaded successfully.
2474	wp_send_json_success( $response );
2475}
2476
2477/**
2478 * Ajax handler for uploading attachments
2479 *
2480 * @since 3.3.0
2481 */
2482function wp_ajax_upload_attachment() {
2483	check_ajax_referer( 'media-form' );
2484	/*
2485	 * This function does not use wp_send_json_success() / wp_send_json_error()
2486	 * as the html4 Plupload handler requires a text/html content-type for older IE.
2487	 * See https://core.trac.wordpress.org/ticket/31037
2488	 */
2489
2490	if ( ! current_user_can( 'upload_files' ) ) {
2491		echo wp_json_encode(
2492			array(
2493				'success' => false,
2494				'data'    => array(
2495					'message'  => __( 'Sorry, you are not allowed to upload files.' ),
2496					'filename' => esc_html( $_FILES['async-upload']['name'] ),
2497				),
2498			)
2499		);
2500
2501		wp_die();
2502	}
2503
2504	if ( isset( $_REQUEST['post_id'] ) ) {
2505		$post_id = $_REQUEST['post_id'];
2506
2507		if ( ! current_user_can( 'edit_post', $post_id ) ) {
2508			echo wp_json_encode(
2509				array(
2510					'success' => false,
2511					'data'    => array(
2512						'message'  => __( 'Sorry, you are not allowed to attach files to this post.' ),
2513						'filename' => esc_html( $_FILES['async-upload']['name'] ),
2514					),
2515				)
2516			);
2517
2518			wp_die();
2519		}
2520	} else {
2521		$post_id = null;
2522	}
2523
2524	$post_data = ! empty( $_REQUEST['post_data'] ) ? _wp_get_allowed_postdata( _wp_translate_postdata( false, (array) $_REQUEST['post_data'] ) ) : array();
2525
2526	if ( is_wp_error( $post_data ) ) {
2527		wp_die( $post_data->get_error_message() );
2528	}
2529
2530	// If the context is custom header or background, make sure the uploaded file is an image.
2531	if ( isset( $post_data['context'] ) && in_array( $post_data['context'], array( 'custom-header', 'custom-background' ), true ) ) {
2532		$wp_filetype = wp_check_filetype_and_ext( $_FILES['async-upload']['tmp_name'], $_FILES['async-upload']['name'] );
2533
2534		if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
2535			echo wp_json_encode(
2536				array(
2537					'success' => false,
2538					'data'    => array(
2539						'message'  => __( 'The uploaded file is not a valid image. Please try again.' ),
2540						'filename' => esc_html( $_FILES['async-upload']['name'] ),
2541					),
2542				)
2543			);
2544
2545			wp_die();
2546		}
2547	}
2548
2549	$attachment_id = media_handle_upload( 'async-upload', $post_id, $post_data );
2550
2551	if ( is_wp_error( $attachment_id ) ) {
2552		echo wp_json_encode(
2553			array(
2554				'success' => false,
2555				'data'    => array(
2556					'message'  => $attachment_id->get_error_message(),
2557					'filename' => esc_html( $_FILES['async-upload']['name'] ),
2558				),
2559			)
2560		);
2561
2562		wp_die();
2563	}
2564
2565	if ( isset( $post_data['context'] ) && isset( $post_data['theme'] ) ) {
2566		if ( 'custom-background' === $post_data['context'] ) {
2567			update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', $post_data['theme'] );
2568		}
2569
2570		if ( 'custom-header' === $post_data['context'] ) {
2571			update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', $post_data['theme'] );
2572		}
2573	}
2574
2575	$attachment = wp_prepare_attachment_for_js( $attachment_id );
2576	if ( ! $attachment ) {
2577		wp_die();
2578	}
2579
2580	echo wp_json_encode(
2581		array(
2582			'success' => true,
2583			'data'    => $attachment,
2584		)
2585	);
2586
2587	wp_die();
2588}
2589
2590/**
2591 * Ajax handler for image editing.
2592 *
2593 * @since 3.1.0
2594 */
2595function wp_ajax_image_editor() {
2596	$attachment_id = (int) $_POST['postid'];
2597
2598	if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
2599		wp_die( -1 );
2600	}
2601
2602	check_ajax_referer( "image_editor-$attachment_id" );
2603	include_once ABSPATH . 'wp-admin/includes/image-edit.php';
2604
2605	$msg = false;
2606
2607	switch ( $_POST['do'] ) {
2608		case 'save':
2609			$msg = wp_save_image( $attachment_id );
2610			if ( ! empty( $msg->error ) ) {
2611				wp_send_json_error( $msg );
2612			}
2613
2614			wp_send_json_success( $msg );
2615			break;
2616		case 'scale':
2617			$msg = wp_save_image( $attachment_id );
2618			break;
2619		case 'restore':
2620			$msg = wp_restore_image( $attachment_id );
2621			break;
2622	}
2623
2624	ob_start();
2625	wp_image_editor( $attachment_id, $msg );
2626	$html = ob_get_clean();
2627
2628	if ( ! empty( $msg->error ) ) {
2629		wp_send_json_error(
2630			array(
2631				'message' => $msg,
2632				'html'    => $html,
2633			)
2634		);
2635	}
2636
2637	wp_send_json_success(
2638		array(
2639			'message' => $msg,
2640			'html'    => $html,
2641		)
2642	);
2643}
2644
2645/**
2646 * Ajax handler for setting the featured image.
2647 *
2648 * @since 3.1.0
2649 */
2650function wp_ajax_set_post_thumbnail() {
2651	$json = ! empty( $_REQUEST['json'] ); // New-style request.
2652
2653	$post_ID = (int) $_POST['post_id'];
2654	if ( ! current_user_can( 'edit_post', $post_ID ) ) {
2655		wp_die( -1 );
2656	}
2657
2658	$thumbnail_id = (int) $_POST['thumbnail_id'];
2659
2660	if ( $json ) {
2661		check_ajax_referer( "update-post_$post_ID" );
2662	} else {
2663		check_ajax_referer( "set_post_thumbnail-$post_ID" );
2664	}
2665
2666	if ( '-1' == $thumbnail_id ) {
2667		if ( delete_post_thumbnail( $post_ID ) ) {
2668			$return = _wp_post_thumbnail_html( null, $post_ID );
2669			$json ? wp_send_json_success( $return ) : wp_die( $return );
2670		} else {
2671			wp_die( 0 );
2672		}
2673	}
2674
2675	if ( set_post_thumbnail( $post_ID, $thumbnail_id ) ) {
2676		$return = _wp_post_thumbnail_html( $thumbnail_id, $post_ID );
2677		$json ? wp_send_json_success( $return ) : wp_die( $return );
2678	}
2679
2680	wp_die( 0 );
2681}
2682
2683/**
2684 * Ajax handler for retrieving HTML for the featured image.
2685 *
2686 * @since 4.6.0
2687 */
2688function wp_ajax_get_post_thumbnail_html() {
2689	$post_ID = (int) $_POST['post_id'];
2690
2691	check_ajax_referer( "update-post_$post_ID" );
2692
2693	if ( ! current_user_can( 'edit_post', $post_ID ) ) {
2694		wp_die( -1 );
2695	}
2696
2697	$thumbnail_id = (int) $_POST['thumbnail_id'];
2698
2699	// For backward compatibility, -1 refers to no featured image.
2700	if ( -1 === $thumbnail_id ) {
2701		$thumbnail_id = null;
2702	}
2703
2704	$return = _wp_post_thumbnail_html( $thumbnail_id, $post_ID );
2705	wp_send_json_success( $return );
2706}
2707
2708/**
2709 * Ajax handler for setting the featured image for an attachment.
2710 *
2711 * @since 4.0.0
2712 *
2713 * @see set_post_thumbnail()
2714 */
2715function wp_ajax_set_attachment_thumbnail() {
2716	if ( empty( $_POST['urls'] ) || ! is_array( $_POST['urls'] ) ) {
2717		wp_send_json_error();
2718	}
2719
2720	$thumbnail_id = (int) $_POST['thumbnail_id'];
2721	if ( empty( $thumbnail_id ) ) {
2722		wp_send_json_error();
2723	}
2724
2725	$post_ids = array();
2726	// For each URL, try to find its corresponding post ID.
2727	foreach ( $_POST['urls'] as $url ) {
2728		$post_id = attachment_url_to_postid( $url );
2729		if ( ! empty( $post_id ) ) {
2730			$post_ids[] = $post_id;
2731		}
2732	}
2733
2734	if ( empty( $post_ids ) ) {
2735		wp_send_json_error();
2736	}
2737
2738	$success = 0;
2739	// For each found attachment, set its thumbnail.
2740	foreach ( $post_ids as $post_id ) {
2741		if ( ! current_user_can( 'edit_post', $post_id ) ) {
2742			continue;
2743		}
2744
2745		if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) {
2746			$success++;
2747		}
2748	}
2749
2750	if ( 0 === $success ) {
2751		wp_send_json_error();
2752	} else {
2753		wp_send_json_success();
2754	}
2755
2756	wp_send_json_error();
2757}
2758
2759/**
2760 * Ajax handler for date formatting.
2761 *
2762 * @since 3.1.0
2763 */
2764function wp_ajax_date_format() {
2765	wp_die( date_i18n( sanitize_option( 'date_format', wp_unslash( $_POST['date'] ) ) ) );
2766}
2767
2768/**
2769 * Ajax handler for time formatting.
2770 *
2771 * @since 3.1.0
2772 */
2773function wp_ajax_time_format() {
2774	wp_die( date_i18n( sanitize_option( 'time_format', wp_unslash( $_POST['date'] ) ) ) );
2775}
2776
2777/**
2778 * Ajax handler for saving posts from the fullscreen editor.
2779 *
2780 * @since 3.1.0
2781 * @deprecated 4.3.0
2782 */
2783function wp_ajax_wp_fullscreen_save_post() {
2784	$post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;
2785
2786	$post = null;
2787
2788	if ( $post_id ) {
2789		$post = get_post( $post_id );
2790	}
2791
2792	check_ajax_referer( 'update-post_' . $post_id, '_wpnonce' );
2793
2794	$post_id = edit_post();
2795
2796	if ( is_wp_error( $post_id ) ) {
2797		wp_send_json_error();
2798	}
2799
2800	if ( $post ) {
2801		$last_date = mysql2date( __( 'F j, Y' ), $post->post_modified );
2802		$last_time = mysql2date( __( 'g:i a' ), $post->post_modified );
2803	} else {
2804		$last_date = date_i18n( __( 'F j, Y' ) );
2805		$last_time = date_i18n( __( 'g:i a' ) );
2806	}
2807
2808	$last_id = get_post_meta( $post_id, '_edit_last', true );
2809	if ( $last_id ) {
2810		$last_user = get_userdata( $last_id );
2811		/* translators: 1: User's display name, 2: Date of last edit, 3: Time of last edit. */
2812		$last_edited = sprintf( __( 'Last edited by %1$s on %2$s at %3$s' ), esc_html( $last_user->display_name ), $last_date, $last_time );
2813	} else {
2814		/* translators: 1: Date of last edit, 2: Time of last edit. */
2815		$last_edited = sprintf( __( 'Last edited on %1$s at %2$s' ), $last_date, $last_time );
2816	}
2817
2818	wp_send_json_success( array( 'last_edited' => $last_edited ) );
2819}
2820
2821/**
2822 * Ajax handler for removing a post lock.
2823 *
2824 * @since 3.1.0
2825 */
2826function wp_ajax_wp_remove_post_lock() {
2827	if ( empty( $_POST['post_ID'] ) || empty( $_POST['active_post_lock'] ) ) {
2828		wp_die( 0 );
2829	}
2830
2831	$post_id = (int) $_POST['post_ID'];
2832	$post    = get_post( $post_id );
2833
2834	if ( ! $post ) {
2835		wp_die( 0 );
2836	}
2837
2838	check_ajax_referer( 'update-post_' . $post_id );
2839
2840	if ( ! current_user_can( 'edit_post', $post_id ) ) {
2841		wp_die( -1 );
2842	}
2843
2844	$active_lock = array_map( 'absint', explode( ':', $_POST['active_post_lock'] ) );
2845
2846	if ( get_current_user_id() != $active_lock[1] ) {
2847		wp_die( 0 );
2848	}
2849
2850	/**
2851	 * Filters the post lock window duration.
2852	 *
2853	 * @since 3.3.0
2854	 *
2855	 * @param int $interval The interval in seconds the post lock duration
2856	 *                      should last, plus 5 seconds. Default 150.
2857	 */
2858	$new_lock = ( time() - apply_filters( 'wp_check_post_lock_window', 150 ) + 5 ) . ':' . $active_lock[1];
2859	update_post_meta( $post_id, '_edit_lock', $new_lock, implode( ':', $active_lock ) );
2860	wp_die( 1 );
2861}
2862
2863/**
2864 * Ajax handler for dismissing a WordPress pointer.
2865 *
2866 * @since 3.1.0
2867 */
2868function wp_ajax_dismiss_wp_pointer() {
2869	$pointer = $_POST['pointer'];
2870
2871	if ( sanitize_key( $pointer ) != $pointer ) {
2872		wp_die( 0 );
2873	}
2874
2875	//  check_ajax_referer( 'dismiss-pointer_' . $pointer );
2876
2877	$dismissed = array_filter( explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) );
2878
2879	if ( in_array( $pointer, $dismissed, true ) ) {
2880		wp_die( 0 );
2881	}
2882
2883	$dismissed[] = $pointer;
2884	$dismissed   = implode( ',', $dismissed );
2885
2886	update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed );
2887	wp_die( 1 );
2888}
2889
2890/**
2891 * Ajax handler for getting an attachment.
2892 *
2893 * @since 3.5.0
2894 */
2895function wp_ajax_get_attachment() {
2896	if ( ! isset( $_REQUEST['id'] ) ) {
2897		wp_send_json_error();
2898	}
2899
2900	$id = absint( $_REQUEST['id'] );
2901	if ( ! $id ) {
2902		wp_send_json_error();
2903	}
2904
2905	$post = get_post( $id );
2906	if ( ! $post ) {
2907		wp_send_json_error();
2908	}
2909
2910	if ( 'attachment' !== $post->post_type ) {
2911		wp_send_json_error();
2912	}
2913
2914	if ( ! current_user_can( 'upload_files' ) ) {
2915		wp_send_json_error();
2916	}
2917
2918	$attachment = wp_prepare_attachment_for_js( $id );
2919	if ( ! $attachment ) {
2920		wp_send_json_error();
2921	}
2922
2923	wp_send_json_success( $attachment );
2924}
2925
2926/**
2927 * Ajax handler for querying attachments.
2928 *
2929 * @since 3.5.0
2930 */
2931function wp_ajax_query_attachments() {
2932	if ( ! current_user_can( 'upload_files' ) ) {
2933		wp_send_json_error();
2934	}
2935
2936	$query = isset( $_REQUEST['query'] ) ? (array) $_REQUEST['query'] : array();
2937	$keys  = array(
2938		's',
2939		'order',
2940		'orderby',
2941		'posts_per_page',
2942		'paged',
2943		'post_mime_type',
2944		'post_parent',
2945		'author',
2946		'post__in',
2947		'post__not_in',
2948		'year',
2949		'monthnum',
2950	);
2951
2952	foreach ( get_taxonomies_for_attachments( 'objects' ) as $t ) {
2953		if ( $t->query_var && isset( $query[ $t->query_var ] ) ) {
2954			$keys[] = $t->query_var;
2955		}
2956	}
2957
2958	$query              = array_intersect_key( $query, array_flip( $keys ) );
2959	$query['post_type'] = 'attachment';
2960
2961	if (
2962		MEDIA_TRASH &&
2963		! empty( $_REQUEST['query']['post_status'] ) &&
2964		'trash' === $_REQUEST['query']['post_status']
2965	) {
2966		$query['post_status'] = 'trash';
2967	} else {
2968		$query['post_status'] = 'inherit';
2969	}
2970
2971	if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) {
2972		$query['post_status'] .= ',private';
2973	}
2974
2975	// Filter query clauses to include filenames.
2976	if ( isset( $query['s'] ) ) {
2977		add_filter( 'posts_clauses', '_filter_query_attachment_filenames' );
2978	}
2979
2980	/**
2981	 * Filters the arguments passed to WP_Query during an Ajax
2982	 * call for querying attachments.
2983	 *
2984	 * @since 3.7.0
2985	 *
2986	 * @see WP_Query::parse_query()
2987	 *
2988	 * @param array $query An array of query variables.
2989	 */
2990	$query             = apply_filters( 'ajax_query_attachments_args', $query );
2991	$attachments_query = new WP_Query( $query );
2992
2993	$posts       = array_map( 'wp_prepare_attachment_for_js', $attachments_query->posts );
2994	$posts       = array_filter( $posts );
2995	$total_posts = $attachments_query->found_posts;
2996
2997	if ( $total_posts < 1 ) {
2998		// Out-of-bounds, run the query again without LIMIT for total count.
2999		unset( $query['paged'] );
3000
3001		$count_query = new WP_Query();
3002		$count_query->query( $query );
3003		$total_posts = $count_query->found_posts;
3004	}
3005
3006	$posts_per_page = (int) $attachments_query->get( 'posts_per_page' );
3007
3008	$max_pages = $posts_per_page ? ceil( $total_posts / $posts_per_page ) : 0;
3009
3010	header( 'X-WP-Total: ' . (int) $total_posts );
3011	header( 'X-WP-TotalPages: ' . (int) $max_pages );
3012
3013	wp_send_json_success( $posts );
3014}
3015
3016/**
3017 * Ajax handler for updating attachment attributes.
3018 *
3019 * @since 3.5.0
3020 */
3021function wp_ajax_save_attachment() {
3022	if ( ! isset( $_REQUEST['id'] ) || ! isset( $_REQUEST['changes'] ) ) {
3023		wp_send_json_error();
3024	}
3025
3026	$id = absint( $_REQUEST['id'] );
3027	if ( ! $id ) {
3028		wp_send_json_error();
3029	}
3030
3031	check_ajax_referer( 'update-post_' . $id, 'nonce' );
3032
3033	if ( ! current_user_can( 'edit_post', $id ) ) {
3034		wp_send_json_error();
3035	}
3036
3037	$changes = $_REQUEST['changes'];
3038	$post    = get_post( $id, ARRAY_A );
3039
3040	if ( 'attachment' !== $post['post_type'] ) {
3041		wp_send_json_error();
3042	}
3043
3044	if ( isset( $changes['parent'] ) ) {
3045		$post['post_parent'] = $changes['parent'];
3046	}
3047
3048	if ( isset( $changes['title'] ) ) {
3049		$post['post_title'] = $changes['title'];
3050	}
3051
3052	if ( isset( $changes['caption'] ) ) {
3053		$post['post_excerpt'] = $changes['caption'];
3054	}
3055
3056	if ( isset( $changes['description'] ) ) {
3057		$post['post_content'] = $changes['description'];
3058	}
3059
3060	if ( MEDIA_TRASH && isset( $changes['status'] ) ) {
3061		$post['post_status'] = $changes['status'];
3062	}
3063
3064	if ( isset( $changes['alt'] ) ) {
3065		$alt = wp_unslash( $changes['alt'] );
3066		if ( get_post_meta( $id, '_wp_attachment_image_alt', true ) !== $alt ) {
3067			$alt = wp_strip_all_tags( $alt, true );
3068			update_post_meta( $id, '_wp_attachment_image_alt', wp_slash( $alt ) );
3069		}
3070	}
3071
3072	if ( wp_attachment_is( 'audio', $post['ID'] ) ) {
3073		$changed = false;
3074		$id3data = wp_get_attachment_metadata( $post['ID'] );
3075
3076		if ( ! is_array( $id3data ) ) {
3077			$changed = true;
3078			$id3data = array();
3079		}
3080
3081		foreach ( wp_get_attachment_id3_keys( (object) $post, 'edit' ) as $key => $label ) {
3082			if ( isset( $changes[ $key ] ) ) {
3083				$changed         = true;
3084				$id3data[ $key ] = sanitize_text_field( wp_unslash( $changes[ $key ] ) );
3085			}
3086		}
3087
3088		if ( $changed ) {
3089			wp_update_attachment_metadata( $id, $id3data );
3090		}
3091	}
3092
3093	if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) {
3094		wp_delete_post( $id );
3095	} else {
3096		wp_update_post( $post );
3097	}
3098
3099	wp_send_json_success();
3100}
3101
3102/**
3103 * Ajax handler for saving backward compatible attachment attributes.
3104 *
3105 * @since 3.5.0
3106 */
3107function wp_ajax_save_attachment_compat() {
3108	if ( ! isset( $_REQUEST['id'] ) ) {
3109		wp_send_json_error();
3110	}
3111
3112	$id = absint( $_REQUEST['id'] );
3113	if ( ! $id ) {
3114		wp_send_json_error();
3115	}
3116
3117	if ( empty( $_REQUEST['attachments'] ) || empty( $_REQUEST['attachments'][ $id ] ) ) {
3118		wp_send_json_error();
3119	}
3120
3121	$attachment_data = $_REQUEST['attachments'][ $id ];
3122
3123	check_ajax_referer( 'update-post_' . $id, 'nonce' );
3124
3125	if ( ! current_user_can( 'edit_post', $id ) ) {
3126		wp_send_json_error();
3127	}
3128
3129	$post = get_post( $id, ARRAY_A );
3130
3131	if ( 'attachment' !== $post['post_type'] ) {
3132		wp_send_json_error();
3133	}
3134
3135	/** This filter is documented in wp-admin/includes/media.php */
3136	$post = apply_filters( 'attachment_fields_to_save', $post, $attachment_data );
3137
3138	if ( isset( $post['errors'] ) ) {
3139		$errors = $post['errors']; // @todo return me and display me!
3140		unset( $post['errors'] );
3141	}
3142
3143	wp_update_post( $post );
3144
3145	foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) {
3146		if ( isset( $attachment_data[ $taxonomy ] ) ) {
3147			wp_set_object_terms( $id, array_map( 'trim', preg_split( '/,+/', $attachment_data[ $taxonomy ] ) ), $taxonomy, false );
3148		}
3149	}
3150
3151	$attachment = wp_prepare_attachment_for_js( $id );
3152
3153	if ( ! $attachment ) {
3154		wp_send_json_error();
3155	}
3156
3157	wp_send_json_success( $attachment );
3158}
3159
3160/**
3161 * Ajax handler for saving the attachment order.
3162 *
3163 * @since 3.5.0
3164 */
3165function wp_ajax_save_attachment_order() {
3166	if ( ! isset( $_REQUEST['post_id'] ) ) {
3167		wp_send_json_error();
3168	}
3169
3170	$post_id = absint( $_REQUEST['post_id'] );
3171	if ( ! $post_id ) {
3172		wp_send_json_error();
3173	}
3174
3175	if ( empty( $_REQUEST['attachments'] ) ) {
3176		wp_send_json_error();
3177	}
3178
3179	check_ajax_referer( 'update-post_' . $post_id, 'nonce' );
3180
3181	$attachments = $_REQUEST['attachments'];
3182
3183	if ( ! current_user_can( 'edit_post', $post_id ) ) {
3184		wp_send_json_error();
3185	}
3186
3187	foreach ( $attachments as $attachment_id => $menu_order ) {
3188		if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
3189			continue;
3190		}
3191
3192		$attachment = get_post( $attachment_id );
3193
3194		if ( ! $attachment ) {
3195			continue;
3196		}
3197
3198		if ( 'attachment' !== $attachment->post_type ) {
3199			continue;
3200		}
3201
3202		wp_update_post(
3203			array(
3204				'ID'         => $attachment_id,
3205				'menu_order' => $menu_order,
3206			)
3207		);
3208	}
3209
3210	wp_send_json_success();
3211}
3212
3213/**
3214 * Ajax handler for sending an attachment to the editor.
3215 *
3216 * Generates the HTML to send an attachment to the editor.
3217 * Backward compatible with the {@see 'media_send_to_editor'} filter
3218 * and the chain of filters that follow.
3219 *
3220 * @since 3.5.0
3221 */
3222function wp_ajax_send_attachment_to_editor() {
3223	check_ajax_referer( 'media-send-to-editor', 'nonce' );
3224
3225	$attachment = wp_unslash( $_POST['attachment'] );
3226
3227	$id = (int) $attachment['id'];
3228
3229	$post = get_post( $id );
3230	if ( ! $post ) {
3231		wp_send_json_error();
3232	}
3233
3234	if ( 'attachment' !== $post->post_type ) {
3235		wp_send_json_error();
3236	}
3237
3238	if ( current_user_can( 'edit_post', $id ) ) {
3239		// If this attachment is unattached, attach it. Primarily a back compat thing.
3240		$insert_into_post_id = (int) $_POST['post_id'];
3241
3242		if ( 0 == $post->post_parent && $insert_into_post_id ) {
3243			wp_update_post(
3244				array(
3245					'ID'          => $id,
3246					'post_parent' => $insert_into_post_id,
3247				)
3248			);
3249		}
3250	}
3251
3252	$url = empty( $attachment['url'] ) ? '' : $attachment['url'];
3253	$rel = ( strpos( $url, 'attachment_id' ) || get_attachment_link( $id ) == $url );
3254
3255	remove_filter( 'media_send_to_editor', 'image_media_send_to_editor' );
3256
3257	if ( 'image' === substr( $post->post_mime_type, 0, 5 ) ) {
3258		$align = isset( $attachment['align'] ) ? $attachment['align'] : 'none';
3259		$size  = isset( $attachment['image-size'] ) ? $attachment['image-size'] : 'medium';
3260		$alt   = isset( $attachment['image_alt'] ) ? $attachment['image_alt'] : '';
3261
3262		// No whitespace-only captions.
3263		$caption = isset( $attachment['post_excerpt'] ) ? $attachment['post_excerpt'] : '';
3264		if ( '' === trim( $caption ) ) {
3265			$caption = '';
3266		}
3267
3268		$title = ''; // We no longer insert title tags into <img> tags, as they are redundant.
3269		$html  = get_image_send_to_editor( $id, $caption, $title, $align, $url, $rel, $size, $alt );
3270	} elseif ( wp_attachment_is( 'video', $post ) || wp_attachment_is( 'audio', $post ) ) {
3271		$html = stripslashes_deep( $_POST['html'] );
3272	} else {
3273		$html = isset( $attachment['post_title'] ) ? $attachment['post_title'] : '';
3274		$rel  = $rel ? ' rel="attachment wp-att-' . $id . '"' : ''; // Hard-coded string, $id is already sanitized.
3275
3276		if ( ! empty( $url ) ) {
3277			$html = '<a href="' . esc_url( $url ) . '"' . $rel . '>' . $html . '</a>';
3278		}
3279	}
3280
3281	/** This filter is documented in wp-admin/includes/media.php */
3282	$html = apply_filters( 'media_send_to_editor', $html, $id, $attachment );
3283
3284	wp_send_json_success( $html );
3285}
3286
3287/**
3288 * Ajax handler for sending a link to the editor.
3289 *
3290 * Generates the HTML to send a non-image embed link to the editor.
3291 *
3292 * Backward compatible with the following filters:
3293 * - file_send_to_editor_url
3294 * - audio_send_to_editor_url
3295 * - video_send_to_editor_url
3296 *
3297 * @since 3.5.0
3298 *
3299 * @global WP_Post  $post     Global post object.
3300 * @global WP_Embed $wp_embed
3301 */
3302function wp_ajax_send_link_to_editor() {
3303	global $post, $wp_embed;
3304
3305	check_ajax_referer( 'media-send-to-editor', 'nonce' );
3306
3307	$src = wp_unslash( $_POST['src'] );
3308	if ( ! $src ) {
3309		wp_send_json_error();
3310	}
3311
3312	if ( ! strpos( $src, '://' ) ) {
3313		$src = 'http://' . $src;
3314	}
3315
3316	$src = esc_url_raw( $src );
3317	if ( ! $src ) {
3318		wp_send_json_error();
3319	}
3320
3321	$link_text = trim( wp_unslash( $_POST['link_text'] ) );
3322	if ( ! $link_text ) {
3323		$link_text = wp_basename( $src );
3324	}
3325
3326	$post = get_post( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 );
3327
3328	// Ping WordPress for an embed.
3329	$check_embed = $wp_embed->run_shortcode( '[embed]' . $src . '[/embed]' );
3330
3331	// Fallback that WordPress creates when no oEmbed was found.
3332	$fallback = $wp_embed->maybe_make_link( $src );
3333
3334	if ( $check_embed !== $fallback ) {
3335		// TinyMCE view for [embed] will parse this.
3336		$html = '[embed]' . $src . '[/embed]';
3337	} elseif ( $link_text ) {
3338		$html = '<a href="' . esc_url( $src ) . '">' . $link_text . '</a>';
3339	} else {
3340		$html = '';
3341	}
3342
3343	// Figure out what filter to run:
3344	$type = 'file';
3345	$ext  = preg_replace( '/^.+?\.([^.]+)$/', '$1', $src );
3346	if ( $ext ) {
3347		$ext_type = wp_ext2type( $ext );
3348		if ( 'audio' === $ext_type || 'video' === $ext_type ) {
3349			$type = $ext_type;
3350		}
3351	}
3352
3353	/** This filter is documented in wp-admin/includes/media.php */
3354	$html = apply_filters( "{$type}_send_to_editor_url", $html, $src, $link_text );
3355
3356	wp_send_json_success( $html );
3357}
3358
3359/**
3360 * Ajax handler for the Heartbeat API.
3361 *
3362 * Runs when the user is logged in.
3363 *
3364 * @since 3.6.0
3365 */
3366function wp_ajax_heartbeat() {
3367	if ( empty( $_POST['_nonce'] ) ) {
3368		wp_send_json_error();
3369	}
3370
3371	$response    = array();
3372	$data        = array();
3373	$nonce_state = wp_verify_nonce( $_POST['_nonce'], 'heartbeat-nonce' );
3374
3375	// 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
3376	if ( ! empty( $_POST['screen_id'] ) ) {
3377		$screen_id = sanitize_key( $_POST['screen_id'] );
3378	} else {
3379		$screen_id = 'front';
3380	}
3381
3382	if ( ! empty( $_POST['data'] ) ) {
3383		$data = wp_unslash( (array) $_POST['data'] );
3384	}
3385
3386	if ( 1 !== $nonce_state ) {
3387		/**
3388		 * Filters the nonces to send to the New/Edit Post screen.
3389		 *
3390		 * @since 4.3.0
3391		 *
3392		 * @param array  $response  The Heartbeat response.
3393		 * @param array  $data      The $_POST data sent.
3394		 * @param string $screen_id The screen ID.
3395		 */
3396		$response = apply_filters( 'wp_refresh_nonces', $response, $data, $screen_id );
3397
3398		if ( false === $nonce_state ) {
3399			// User is logged in but nonces have expired.
3400			$response['nonces_expired'] = true;
3401			wp_send_json( $response );
3402		}
3403	}
3404
3405	if ( ! empty( $data ) ) {
3406		/**
3407		 * Filters the Heartbeat response received.
3408		 *
3409		 * @since 3.6.0
3410		 *
3411		 * @param array  $response  The Heartbeat response.
3412		 * @param array  $data      The $_POST data sent.
3413		 * @param string $screen_id The screen ID.
3414		 */
3415		$response = apply_filters( 'heartbeat_received', $response, $data, $screen_id );
3416	}
3417
3418	/**
3419	 * Filters the Heartbeat response sent.
3420	 *
3421	 * @since 3.6.0
3422	 *
3423	 * @param array  $response  The Heartbeat response.
3424	 * @param string $screen_id The screen ID.
3425	 */
3426	$response = apply_filters( 'heartbeat_send', $response, $screen_id );
3427
3428	/**
3429	 * Fires when Heartbeat ticks in logged-in environments.
3430	 *
3431	 * Allows the transport to be easily replaced with long-polling.
3432	 *
3433	 * @since 3.6.0
3434	 *
3435	 * @param array  $response  The Heartbeat response.
3436	 * @param string $screen_id The screen ID.
3437	 */
3438	do_action( 'heartbeat_tick', $response, $screen_id );
3439
3440	// Send the current time according to the server.
3441	$response['server_time'] = time();
3442
3443	wp_send_json( $response );
3444}
3445
3446/**
3447 * Ajax handler for getting revision diffs.
3448 *
3449 * @since 3.6.0
3450 */
3451function wp_ajax_get_revision_diffs() {
3452	require ABSPATH . 'wp-admin/includes/revision.php';
3453
3454	$post = get_post( (int) $_REQUEST['post_id'] );
3455	if ( ! $post ) {
3456		wp_send_json_error();
3457	}
3458
3459	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
3460		wp_send_json_error();
3461	}
3462
3463	// Really just pre-loading the cache here.
3464	$revisions = wp_get_post_revisions( $post->ID, array( 'check_enabled' => false ) );
3465	if ( ! $revisions ) {
3466		wp_send_json_error();
3467	}
3468
3469	$return = array();
3470	set_time_limit( 0 );
3471
3472	foreach ( $_REQUEST['compare'] as $compare_key ) {
3473		list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to
3474
3475		$return[] = array(
3476			'id'     => $compare_key,
3477			'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ),
3478		);
3479	}
3480	wp_send_json_success( $return );
3481}
3482
3483/**
3484 * Ajax handler for auto-saving the selected color scheme for
3485 * a user's own profile.
3486 *
3487 * @since 3.8.0
3488 *
3489 * @global array $_wp_admin_css_colors
3490 */
3491function wp_ajax_save_user_color_scheme() {
3492	global $_wp_admin_css_colors;
3493
3494	check_ajax_referer( 'save-color-scheme', 'nonce' );
3495
3496	$color_scheme = sanitize_key( $_POST['color_scheme'] );
3497
3498	if ( ! isset( $_wp_admin_css_colors[ $color_scheme ] ) ) {
3499		wp_send_json_error();
3500	}
3501
3502	$previous_color_scheme = get_user_meta( get_current_user_id(), 'admin_color', true );
3503	update_user_meta( get_current_user_id(), 'admin_color', $color_scheme );
3504
3505	wp_send_json_success(
3506		array(
3507			'previousScheme' => 'admin-color-' . $previous_color_scheme,
3508			'currentScheme'  => 'admin-color-' . $color_scheme,
3509		)
3510	);
3511}
3512
3513/**
3514 * Ajax handler for getting themes from themes_api().
3515 *
3516 * @since 3.9.0
3517 *
3518 * @global array $themes_allowedtags
3519 * @global array $theme_field_defaults
3520 */
3521function wp_ajax_query_themes() {
3522	global $themes_allowedtags, $theme_field_defaults;
3523
3524	if ( ! current_user_can( 'install_themes' ) ) {
3525		wp_send_json_error();
3526	}
3527
3528	$args = wp_parse_args(
3529		wp_unslash( $_REQUEST['request'] ),
3530		array(
3531			'per_page' => 20,
3532			'fields'   => array_merge(
3533				(array) $theme_field_defaults,
3534				array(
3535					'reviews_url' => true, // Explicitly request the reviews URL to be linked from the Add Themes screen.
3536				)
3537			),
3538		)
3539	);
3540
3541	if ( isset( $args['browse'] ) && 'favorites' === $args['browse'] && ! isset( $args['user'] ) ) {
3542		$user = get_user_option( 'wporg_favorites' );
3543		if ( $user ) {
3544			$args['user'] = $user;
3545		}
3546	}
3547
3548	$old_filter = isset( $args['browse'] ) ? $args['browse'] : 'search';
3549
3550	/** This filter is documented in wp-admin/includes/class-wp-theme-install-list-table.php */
3551	$args = apply_filters( 'install_themes_table_api_args_' . $old_filter, $args );
3552
3553	$api = themes_api( 'query_themes', $args );
3554
3555	if ( is_wp_error( $api ) ) {
3556		wp_send_json_error();
3557	}
3558
3559	$update_php = network_admin_url( 'update.php?action=install-theme' );
3560
3561	foreach ( $api->themes as &$theme ) {
3562		$theme->install_url = add_query_arg(
3563			array(
3564				'theme'    => $theme->slug,
3565				'_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
3566			),
3567			$update_php
3568		);
3569
3570		if ( current_user_can( 'switch_themes' ) ) {
3571			if ( is_multisite() ) {
3572				$theme->activate_url = add_query_arg(
3573					array(
3574						'action'   => 'enable',
3575						'_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ),
3576						'theme'    => $theme->slug,
3577					),
3578					network_admin_url( 'themes.php' )
3579				);
3580			} else {
3581				$theme->activate_url = add_query_arg(
3582					array(
3583						'action'     => 'activate',
3584						'_wpnonce'   => wp_create_nonce( 'switch-theme_' . $theme->slug ),
3585						'stylesheet' => $theme->slug,
3586					),
3587					admin_url( 'themes.php' )
3588				);
3589			}
3590		}
3591
3592		if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
3593			$theme->customize_url = add_query_arg(
3594				array(
3595					'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
3596				),
3597				wp_customize_url( $theme->slug )
3598			);
3599		}
3600
3601		$theme->name        = wp_kses( $theme->name, $themes_allowedtags );
3602		$theme->author      = wp_kses( $theme->author['display_name'], $themes_allowedtags );
3603		$theme->version     = wp_kses( $theme->version, $themes_allowedtags );
3604		$theme->description = wp_kses( $theme->description, $themes_allowedtags );
3605
3606		$theme->stars = wp_star_rating(
3607			array(
3608				'rating' => $theme->rating,
3609				'type'   => 'percent',
3610				'number' => $theme->num_ratings,
3611				'echo'   => false,
3612			)
3613		);
3614
3615		$theme->num_ratings    = number_format_i18n( $theme->num_ratings );
3616		$theme->preview_url    = set_url_scheme( $theme->preview_url );
3617		$theme->compatible_wp  = is_wp_version_compatible( $theme->requires );
3618		$theme->compatible_php = is_php_version_compatible( $theme->requires_php );
3619	}
3620
3621	wp_send_json_success( $api );
3622}
3623
3624/**
3625 * Apply [embed] Ajax handlers to a string.
3626 *
3627 * @since 4.0.0
3628 *
3629 * @global WP_Post    $post       Global post object.
3630 * @global WP_Embed   $wp_embed   Embed API instance.
3631 * @global WP_Scripts $wp_scripts
3632 * @global int        $content_width
3633 */
3634function wp_ajax_parse_embed() {
3635	global $post, $wp_embed, $content_width;
3636
3637	if ( empty( $_POST['shortcode'] ) ) {
3638		wp_send_json_error();
3639	}
3640
3641	$post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;
3642
3643	if ( $post_id > 0 ) {
3644		$post = get_post( $post_id );
3645
3646		if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
3647			wp_send_json_error();
3648		}
3649		setup_postdata( $post );
3650	} elseif ( ! current_user_can( 'edit_posts' ) ) { // See WP_oEmbed_Controller::get_proxy_item_permissions_check().
3651		wp_send_json_error();
3652	}
3653
3654	$shortcode = wp_unslash( $_POST['shortcode'] );
3655
3656	preg_match( '/' . get_shortcode_regex() . '/s', $shortcode, $matches );
3657	$atts = shortcode_parse_atts( $matches[3] );
3658
3659	if ( ! empty( $matches[5] ) ) {
3660		$url = $matches[5];
3661	} elseif ( ! empty( $atts['src'] ) ) {
3662		$url = $atts['src'];
3663	} else {
3664		$url = '';
3665	}
3666
3667	$parsed                         = false;
3668	$wp_embed->return_false_on_fail = true;
3669
3670	if ( 0 === $post_id ) {
3671		/*
3672		 * Refresh oEmbeds cached outside of posts that are past their TTL.
3673		 * Posts are excluded because they have separate logic for refreshing
3674		 * their post meta caches. See WP_Embed::cache_oembed().
3675		 */
3676		$wp_embed->usecache = false;
3677	}
3678
3679	if ( is_ssl() && 0 === strpos( $url, 'http://' ) ) {
3680		// Admin is ssl and the user pasted non-ssl URL.
3681		// Check if the provider supports ssl embeds and use that for the preview.
3682		$ssl_shortcode = preg_replace( '%^(\\[embed[^\\]]*\\])http://%i', '$1https://', $shortcode );
3683		$parsed        = $wp_embed->run_shortcode( $ssl_shortcode );
3684
3685		if ( ! $parsed ) {
3686			$no_ssl_support = true;
3687		}
3688	}
3689
3690	// Set $content_width so any embeds fit in the destination iframe.
3691	if ( isset( $_POST['maxwidth'] ) && is_numeric( $_POST['maxwidth'] ) && $_POST['maxwidth'] > 0 ) {
3692		if ( ! isset( $content_width ) ) {
3693			$content_width = (int) $_POST['maxwidth'];
3694		} else {
3695			$content_width = min( $content_width, (int) $_POST['maxwidth'] );
3696		}
3697	}
3698
3699	if ( $url && ! $parsed ) {
3700		$parsed = $wp_embed->run_shortcode( $shortcode );
3701	}
3702
3703	if ( ! $parsed ) {
3704		wp_send_json_error(
3705			array(
3706				'type'    => 'not-embeddable',
3707				/* translators: %s: URL that could not be embedded. */
3708				'message' => sprintf( __( '%s failed to embed.' ), '<code>' . esc_html( $url ) . '</code>' ),
3709			)
3710		);
3711	}
3712
3713	if ( has_shortcode( $parsed, 'audio' ) || has_shortcode( $parsed, 'video' ) ) {
3714		$styles     = '';
3715		$mce_styles = wpview_media_sandbox_styles();
3716
3717		foreach ( $mce_styles as $style ) {
3718			$styles .= sprintf( '<link rel="stylesheet" href="%s" />', $style );
3719		}
3720
3721		$html = do_shortcode( $parsed );
3722
3723		global $wp_scripts;
3724
3725		if ( ! empty( $wp_scripts ) ) {
3726			$wp_scripts->done = array();
3727		}
3728
3729		ob_start();
3730		wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
3731		$scripts = ob_get_clean();
3732
3733		$parsed = $styles . $html . $scripts;
3734	}
3735
3736	if ( ! empty( $no_ssl_support ) || ( is_ssl() && ( preg_match( '%<(iframe|script|embed) [^>]*src="http://%', $parsed ) ||
3737		preg_match( '%<link [^>]*href="http://%', $parsed ) ) ) ) {
3738		// Admin is ssl and the embed is not. Iframes, scripts, and other "active content" will be blocked.
3739		wp_send_json_error(
3740			array(
3741				'type'    => 'not-ssl',
3742				'message' => __( 'This preview is unavailable in the editor.' ),
3743			)
3744		);
3745	}
3746
3747	$return = array(
3748		'body' => $parsed,
3749		'attr' => $wp_embed->last_attr,
3750	);
3751
3752	if ( strpos( $parsed, 'class="wp-embedded-content' ) ) {
3753		if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
3754			$script_src = includes_url( 'js/wp-embed.js' );
3755		} else {
3756			$script_src = includes_url( 'js/wp-embed.min.js' );
3757		}
3758
3759		$return['head']    = '<script src="' . $script_src . '"></script>';
3760		$return['sandbox'] = true;
3761	}
3762
3763	wp_send_json_success( $return );
3764}
3765
3766/**
3767 * @since 4.0.0
3768 *
3769 * @global WP_Post    $post       Global post object.
3770 * @global WP_Scripts $wp_scripts
3771 */
3772function wp_ajax_parse_media_shortcode() {
3773	global $post, $wp_scripts;
3774
3775	if ( empty( $_POST['shortcode'] ) ) {
3776		wp_send_json_error();
3777	}
3778
3779	$shortcode = wp_unslash( $_POST['shortcode'] );
3780
3781	if ( ! empty( $_POST['post_ID'] ) ) {
3782		$post = get_post( (int) $_POST['post_ID'] );
3783	}
3784
3785	// The embed shortcode requires a post.
3786	if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
3787		if ( 'embed' === $shortcode ) {
3788			wp_send_json_error();
3789		}
3790	} else {
3791		setup_postdata( $post );
3792	}
3793
3794	$parsed = do_shortcode( $shortcode );
3795
3796	if ( empty( $parsed ) ) {
3797		wp_send_json_error(
3798			array(
3799				'type'    => 'no-items',
3800				'message' => __( 'No items found.' ),
3801			)
3802		);
3803	}
3804
3805	$head   = '';
3806	$styles = wpview_media_sandbox_styles();
3807
3808	foreach ( $styles as $style ) {
3809		$head .= '<link type="text/css" rel="stylesheet" href="' . $style . '">';
3810	}
3811
3812	if ( ! empty( $wp_scripts ) ) {
3813		$wp_scripts->done = array();
3814	}
3815
3816	ob_start();
3817
3818	echo $parsed;
3819
3820	if ( 'playlist' === $_REQUEST['type'] ) {
3821		wp_underscore_playlist_templates();
3822
3823		wp_print_scripts( 'wp-playlist' );
3824	} else {
3825		wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
3826	}
3827
3828	wp_send_json_success(
3829		array(
3830			'head' => $head,
3831			'body' => ob_get_clean(),
3832		)
3833	);
3834}
3835
3836/**
3837 * Ajax handler for destroying multiple open sessions for a user.
3838 *
3839 * @since 4.1.0
3840 */
3841function wp_ajax_destroy_sessions() {
3842	$user = get_userdata( (int) $_POST['user_id'] );
3843
3844	if ( $user ) {
3845		if ( ! current_user_can( 'edit_user', $user->ID ) ) {
3846			$user = false;
3847		} elseif ( ! wp_verify_nonce( $_POST['nonce'], 'update-user_' . $user->ID ) ) {
3848			$user = false;
3849		}
3850	}
3851
3852	if ( ! $user ) {
3853		wp_send_json_error(
3854			array(
3855				'message' => __( 'Could not log out user sessions. Please try again.' ),
3856			)
3857		);
3858	}
3859
3860	$sessions = WP_Session_Tokens::get_instance( $user->ID );
3861
3862	if ( get_current_user_id() === $user->ID ) {
3863		$sessions->destroy_others( wp_get_session_token() );
3864		$message = __( 'You are now logged out everywhere else.' );
3865	} else {
3866		$sessions->destroy_all();
3867		/* translators: %s: User's display name. */
3868		$message = sprintf( __( '%s has been logged out.' ), $user->display_name );
3869	}
3870
3871	wp_send_json_success( array( 'message' => $message ) );
3872}
3873
3874/**
3875 * Ajax handler for cropping an image.
3876 *
3877 * @since 4.3.0
3878 */
3879function wp_ajax_crop_image() {
3880	$attachment_id = absint( $_POST['id'] );
3881
3882	check_ajax_referer( 'image_editor-' . $attachment_id, 'nonce' );
3883
3884	if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
3885		wp_send_json_error();
3886	}
3887
3888	$context = str_replace( '_', '-', $_POST['context'] );
3889	$data    = array_map( 'absint', $_POST['cropDetails'] );
3890	$cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
3891
3892	if ( ! $cropped || is_wp_error( $cropped ) ) {
3893		wp_send_json_error( array( 'message' => __( 'Image could not be processed.' ) ) );
3894	}
3895
3896	switch ( $context ) {
3897		case 'site-icon':
3898			require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php';
3899			$wp_site_icon = new WP_Site_Icon();
3900
3901			// Skip creating a new attachment if the attachment is a Site Icon.
3902			if ( get_post_meta( $attachment_id, '_wp_attachment_context', true ) == $context ) {
3903
3904				// Delete the temporary cropped file, we don't need it.
3905				wp_delete_file( $cropped );
3906
3907				// Additional sizes in wp_prepare_attachment_for_js().
3908				add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
3909				break;
3910			}
3911
3912			/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
3913			$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
3914			$object  = $wp_site_icon->create_attachment_object( $cropped, $attachment_id );
3915			unset( $object['ID'] );
3916
3917			// Update the attachment.
3918			add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
3919			$attachment_id = $wp_site_icon->insert_attachment( $object, $cropped );
3920			remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
3921
3922			// Additional sizes in wp_prepare_attachment_for_js().
3923			add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
3924			break;
3925
3926		default:
3927			/**
3928			 * Fires before a cropped image is saved.
3929			 *
3930			 * Allows to add filters to modify the way a cropped image is saved.
3931			 *
3932			 * @since 4.3.0
3933			 *
3934			 * @param string $context       The Customizer control requesting the cropped image.
3935			 * @param int    $attachment_id The attachment ID of the original image.
3936			 * @param string $cropped       Path to the cropped image file.
3937			 */
3938			do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped );
3939
3940			/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
3941			$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
3942
3943			$parent_url = wp_get_attachment_url( $attachment_id );
3944			$url        = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url );
3945
3946			$size       = wp_getimagesize( $cropped );
3947			$image_type = ( $size ) ? $size['mime'] : 'image/jpeg';
3948
3949			$object = array(
3950				'post_title'     => wp_basename( $cropped ),
3951				'post_content'   => $url,
3952				'post_mime_type' => $image_type,
3953				'guid'           => $url,
3954				'context'        => $context,
3955			);
3956
3957			$attachment_id = wp_insert_attachment( $object, $cropped );
3958			$metadata      = wp_generate_attachment_metadata( $attachment_id, $cropped );
3959
3960			/**
3961			 * Filters the cropped image attachment metadata.
3962			 *
3963			 * @since 4.3.0
3964			 *
3965			 * @see wp_generate_attachment_metadata()
3966			 *
3967			 * @param array $metadata Attachment metadata.
3968			 */
3969			$metadata = apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata );
3970			wp_update_attachment_metadata( $attachment_id, $metadata );
3971
3972			/**
3973			 * Filters the attachment ID for a cropped image.
3974			 *
3975			 * @since 4.3.0
3976			 *
3977			 * @param int    $attachment_id The attachment ID of the cropped image.
3978			 * @param string $context       The Customizer control requesting the cropped image.
3979			 */
3980			$attachment_id = apply_filters( 'wp_ajax_cropped_attachment_id', $attachment_id, $context );
3981	}
3982
3983	wp_send_json_success( wp_prepare_attachment_for_js( $attachment_id ) );
3984}
3985
3986/**
3987 * Ajax handler for generating a password.
3988 *
3989 * @since 4.4.0
3990 */
3991function wp_ajax_generate_password() {
3992	wp_send_json_success( wp_generate_password( 24 ) );
3993}
3994
3995/**
3996 * Ajax handler for generating a password in the no-privilege context.
3997 *
3998 * @since 5.7.0
3999 */
4000function wp_ajax_nopriv_generate_password() {
4001	wp_send_json_success( wp_generate_password( 24 ) );
4002}
4003
4004/**
4005 * Ajax handler for saving the user's WordPress.org username.
4006 *
4007 * @since 4.4.0
4008 */
4009function wp_ajax_save_wporg_username() {
4010	if ( ! current_user_can( 'install_themes' ) && ! current_user_can( 'install_plugins' ) ) {
4011		wp_send_json_error();
4012	}
4013
4014	check_ajax_referer( 'save_wporg_username_' . get_current_user_id() );
4015
4016	$username = isset( $_REQUEST['username'] ) ? wp_unslash( $_REQUEST['username'] ) : false;
4017
4018	if ( ! $username ) {
4019		wp_send_json_error();
4020	}
4021
4022	wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) );
4023}
4024
4025/**
4026 * Ajax handler for installing a theme.
4027 *
4028 * @since 4.6.0
4029 *
4030 * @see Theme_Upgrader
4031 *
4032 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4033 */
4034function wp_ajax_install_theme() {
4035	check_ajax_referer( 'updates' );
4036
4037	if ( empty( $_POST['slug'] ) ) {
4038		wp_send_json_error(
4039			array(
4040				'slug'         => '',
4041				'errorCode'    => 'no_theme_specified',
4042				'errorMessage' => __( 'No theme specified.' ),
4043			)
4044		);
4045	}
4046
4047	$slug = sanitize_key( wp_unslash( $_POST['slug'] ) );
4048
4049	$status = array(
4050		'install' => 'theme',
4051		'slug'    => $slug,
4052	);
4053
4054	if ( ! current_user_can( 'install_themes' ) ) {
4055		$status['errorMessage'] = __( 'Sorry, you are not allowed to install themes on this site.' );
4056		wp_send_json_error( $status );
4057	}
4058
4059	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4060	include_once ABSPATH . 'wp-admin/includes/theme.php';
4061
4062	$api = themes_api(
4063		'theme_information',
4064		array(
4065			'slug'   => $slug,
4066			'fields' => array( 'sections' => false ),
4067		)
4068	);
4069
4070	if ( is_wp_error( $api ) ) {
4071		$status['errorMessage'] = $api->get_error_message();
4072		wp_send_json_error( $status );
4073	}
4074
4075	$skin     = new WP_Ajax_Upgrader_Skin();
4076	$upgrader = new Theme_Upgrader( $skin );
4077	$result   = $upgrader->install( $api->download_link );
4078
4079	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4080		$status['debug'] = $skin->get_upgrade_messages();
4081	}
4082
4083	if ( is_wp_error( $result ) ) {
4084		$status['errorCode']    = $result->get_error_code();
4085		$status['errorMessage'] = $result->get_error_message();
4086		wp_send_json_error( $status );
4087	} elseif ( is_wp_error( $skin->result ) ) {
4088		$status['errorCode']    = $skin->result->get_error_code();
4089		$status['errorMessage'] = $skin->result->get_error_message();
4090		wp_send_json_error( $status );
4091	} elseif ( $skin->get_errors()->has_errors() ) {
4092		$status['errorMessage'] = $skin->get_error_messages();
4093		wp_send_json_error( $status );
4094	} elseif ( is_null( $result ) ) {
4095		global $wp_filesystem;
4096
4097		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4098		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4099
4100		// Pass through the error from WP_Filesystem if one was raised.
4101		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4102			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4103		}
4104
4105		wp_send_json_error( $status );
4106	}
4107
4108	$status['themeName'] = wp_get_theme( $slug )->get( 'Name' );
4109
4110	if ( current_user_can( 'switch_themes' ) ) {
4111		if ( is_multisite() ) {
4112			$status['activateUrl'] = add_query_arg(
4113				array(
4114					'action'   => 'enable',
4115					'_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ),
4116					'theme'    => $slug,
4117				),
4118				network_admin_url( 'themes.php' )
4119			);
4120		} else {
4121			$status['activateUrl'] = add_query_arg(
4122				array(
4123					'action'     => 'activate',
4124					'_wpnonce'   => wp_create_nonce( 'switch-theme_' . $slug ),
4125					'stylesheet' => $slug,
4126				),
4127				admin_url( 'themes.php' )
4128			);
4129		}
4130	}
4131
4132	if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
4133		$status['customizeUrl'] = add_query_arg(
4134			array(
4135				'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
4136			),
4137			wp_customize_url( $slug )
4138		);
4139	}
4140
4141	/*
4142	 * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check
4143	 * on post-installation status.
4144	 */
4145	wp_send_json_success( $status );
4146}
4147
4148/**
4149 * Ajax handler for updating a theme.
4150 *
4151 * @since 4.6.0
4152 *
4153 * @see Theme_Upgrader
4154 *
4155 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4156 */
4157function wp_ajax_update_theme() {
4158	check_ajax_referer( 'updates' );
4159
4160	if ( empty( $_POST['slug'] ) ) {
4161		wp_send_json_error(
4162			array(
4163				'slug'         => '',
4164				'errorCode'    => 'no_theme_specified',
4165				'errorMessage' => __( 'No theme specified.' ),
4166			)
4167		);
4168	}
4169
4170	$stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
4171	$status     = array(
4172		'update'     => 'theme',
4173		'slug'       => $stylesheet,
4174		'oldVersion' => '',
4175		'newVersion' => '',
4176	);
4177
4178	if ( ! current_user_can( 'update_themes' ) ) {
4179		$status['errorMessage'] = __( 'Sorry, you are not allowed to update themes for this site.' );
4180		wp_send_json_error( $status );
4181	}
4182
4183	$theme = wp_get_theme( $stylesheet );
4184	if ( $theme->exists() ) {
4185		$status['oldVersion'] = $theme->get( 'Version' );
4186	}
4187
4188	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4189
4190	$current = get_site_transient( 'update_themes' );
4191	if ( empty( $current ) ) {
4192		wp_update_themes();
4193	}
4194
4195	$skin     = new WP_Ajax_Upgrader_Skin();
4196	$upgrader = new Theme_Upgrader( $skin );
4197	$result   = $upgrader->bulk_upgrade( array( $stylesheet ) );
4198
4199	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4200		$status['debug'] = $skin->get_upgrade_messages();
4201	}
4202
4203	if ( is_wp_error( $skin->result ) ) {
4204		$status['errorCode']    = $skin->result->get_error_code();
4205		$status['errorMessage'] = $skin->result->get_error_message();
4206		wp_send_json_error( $status );
4207	} elseif ( $skin->get_errors()->has_errors() ) {
4208		$status['errorMessage'] = $skin->get_error_messages();
4209		wp_send_json_error( $status );
4210	} elseif ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) {
4211
4212		// Theme is already at the latest version.
4213		if ( true === $result[ $stylesheet ] ) {
4214			$status['errorMessage'] = $upgrader->strings['up_to_date'];
4215			wp_send_json_error( $status );
4216		}
4217
4218		$theme = wp_get_theme( $stylesheet );
4219		if ( $theme->exists() ) {
4220			$status['newVersion'] = $theme->get( 'Version' );
4221		}
4222
4223		wp_send_json_success( $status );
4224	} elseif ( false === $result ) {
4225		global $wp_filesystem;
4226
4227		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4228		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4229
4230		// Pass through the error from WP_Filesystem if one was raised.
4231		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4232			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4233		}
4234
4235		wp_send_json_error( $status );
4236	}
4237
4238	// An unhandled error occurred.
4239	$status['errorMessage'] = __( 'Theme update failed.' );
4240	wp_send_json_error( $status );
4241}
4242
4243/**
4244 * Ajax handler for deleting a theme.
4245 *
4246 * @since 4.6.0
4247 *
4248 * @see delete_theme()
4249 *
4250 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4251 */
4252function wp_ajax_delete_theme() {
4253	check_ajax_referer( 'updates' );
4254
4255	if ( empty( $_POST['slug'] ) ) {
4256		wp_send_json_error(
4257			array(
4258				'slug'         => '',
4259				'errorCode'    => 'no_theme_specified',
4260				'errorMessage' => __( 'No theme specified.' ),
4261			)
4262		);
4263	}
4264
4265	$stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
4266	$status     = array(
4267		'delete' => 'theme',
4268		'slug'   => $stylesheet,
4269	);
4270
4271	if ( ! current_user_can( 'delete_themes' ) ) {
4272		$status['errorMessage'] = __( 'Sorry, you are not allowed to delete themes on this site.' );
4273		wp_send_json_error( $status );
4274	}
4275
4276	if ( ! wp_get_theme( $stylesheet )->exists() ) {
4277		$status['errorMessage'] = __( 'The requested theme does not exist.' );
4278		wp_send_json_error( $status );
4279	}
4280
4281	// Check filesystem credentials. `delete_theme()` will bail otherwise.
4282	$url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
4283
4284	ob_start();
4285	$credentials = request_filesystem_credentials( $url );
4286	ob_end_clean();
4287
4288	if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
4289		global $wp_filesystem;
4290
4291		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4292		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4293
4294		// Pass through the error from WP_Filesystem if one was raised.
4295		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4296			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4297		}
4298
4299		wp_send_json_error( $status );
4300	}
4301
4302	include_once ABSPATH . 'wp-admin/includes/theme.php';
4303
4304	$result = delete_theme( $stylesheet );
4305
4306	if ( is_wp_error( $result ) ) {
4307		$status['errorMessage'] = $result->get_error_message();
4308		wp_send_json_error( $status );
4309	} elseif ( false === $result ) {
4310		$status['errorMessage'] = __( 'Theme could not be deleted.' );
4311		wp_send_json_error( $status );
4312	}
4313
4314	wp_send_json_success( $status );
4315}
4316
4317/**
4318 * Ajax handler for installing a plugin.
4319 *
4320 * @since 4.6.0
4321 *
4322 * @see Plugin_Upgrader
4323 *
4324 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4325 */
4326function wp_ajax_install_plugin() {
4327	check_ajax_referer( 'updates' );
4328
4329	if ( empty( $_POST['slug'] ) ) {
4330		wp_send_json_error(
4331			array(
4332				'slug'         => '',
4333				'errorCode'    => 'no_plugin_specified',
4334				'errorMessage' => __( 'No plugin specified.' ),
4335			)
4336		);
4337	}
4338
4339	$status = array(
4340		'install' => 'plugin',
4341		'slug'    => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4342	);
4343
4344	if ( ! current_user_can( 'install_plugins' ) ) {
4345		$status['errorMessage'] = __( 'Sorry, you are not allowed to install plugins on this site.' );
4346		wp_send_json_error( $status );
4347	}
4348
4349	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4350	include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
4351
4352	$api = plugins_api(
4353		'plugin_information',
4354		array(
4355			'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4356			'fields' => array(
4357				'sections' => false,
4358			),
4359		)
4360	);
4361
4362	if ( is_wp_error( $api ) ) {
4363		$status['errorMessage'] = $api->get_error_message();
4364		wp_send_json_error( $status );
4365	}
4366
4367	$status['pluginName'] = $api->name;
4368
4369	$skin     = new WP_Ajax_Upgrader_Skin();
4370	$upgrader = new Plugin_Upgrader( $skin );
4371	$result   = $upgrader->install( $api->download_link );
4372
4373	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4374		$status['debug'] = $skin->get_upgrade_messages();
4375	}
4376
4377	if ( is_wp_error( $result ) ) {
4378		$status['errorCode']    = $result->get_error_code();
4379		$status['errorMessage'] = $result->get_error_message();
4380		wp_send_json_error( $status );
4381	} elseif ( is_wp_error( $skin->result ) ) {
4382		$status['errorCode']    = $skin->result->get_error_code();
4383		$status['errorMessage'] = $skin->result->get_error_message();
4384		wp_send_json_error( $status );
4385	} elseif ( $skin->get_errors()->has_errors() ) {
4386		$status['errorMessage'] = $skin->get_error_messages();
4387		wp_send_json_error( $status );
4388	} elseif ( is_null( $result ) ) {
4389		global $wp_filesystem;
4390
4391		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4392		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4393
4394		// Pass through the error from WP_Filesystem if one was raised.
4395		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4396			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4397		}
4398
4399		wp_send_json_error( $status );
4400	}
4401
4402	$install_status = install_plugin_install_status( $api );
4403	$pagenow        = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4404
4405	// If installation request is coming from import page, do not return network activation link.
4406	$plugins_url = ( 'import' === $pagenow ) ? admin_url( 'plugins.php' ) : network_admin_url( 'plugins.php' );
4407
4408	if ( current_user_can( 'activate_plugin', $install_status['file'] ) && is_plugin_inactive( $install_status['file'] ) ) {
4409		$status['activateUrl'] = add_query_arg(
4410			array(
4411				'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ),
4412				'action'   => 'activate',
4413				'plugin'   => $install_status['file'],
4414			),
4415			$plugins_url
4416		);
4417	}
4418
4419	if ( is_multisite() && current_user_can( 'manage_network_plugins' ) && 'import' !== $pagenow ) {
4420		$status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
4421	}
4422
4423	wp_send_json_success( $status );
4424}
4425
4426/**
4427 * Ajax handler for updating a plugin.
4428 *
4429 * @since 4.2.0
4430 *
4431 * @see Plugin_Upgrader
4432 *
4433 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4434 */
4435function wp_ajax_update_plugin() {
4436	check_ajax_referer( 'updates' );
4437
4438	if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) {
4439		wp_send_json_error(
4440			array(
4441				'slug'         => '',
4442				'errorCode'    => 'no_plugin_specified',
4443				'errorMessage' => __( 'No plugin specified.' ),
4444			)
4445		);
4446	}
4447
4448	$plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
4449
4450	$status = array(
4451		'update'     => 'plugin',
4452		'slug'       => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4453		'oldVersion' => '',
4454		'newVersion' => '',
4455	);
4456
4457	if ( ! current_user_can( 'update_plugins' ) || 0 !== validate_file( $plugin ) ) {
4458		$status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' );
4459		wp_send_json_error( $status );
4460	}
4461
4462	$plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
4463	$status['plugin']     = $plugin;
4464	$status['pluginName'] = $plugin_data['Name'];
4465
4466	if ( $plugin_data['Version'] ) {
4467		/* translators: %s: Plugin version. */
4468		$status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
4469	}
4470
4471	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
4472
4473	wp_update_plugins();
4474
4475	$skin     = new WP_Ajax_Upgrader_Skin();
4476	$upgrader = new Plugin_Upgrader( $skin );
4477	$result   = $upgrader->bulk_upgrade( array( $plugin ) );
4478
4479	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
4480		$status['debug'] = $skin->get_upgrade_messages();
4481	}
4482
4483	if ( is_wp_error( $skin->result ) ) {
4484		$status['errorCode']    = $skin->result->get_error_code();
4485		$status['errorMessage'] = $skin->result->get_error_message();
4486		wp_send_json_error( $status );
4487	} elseif ( $skin->get_errors()->has_errors() ) {
4488		$status['errorMessage'] = $skin->get_error_messages();
4489		wp_send_json_error( $status );
4490	} elseif ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) {
4491
4492		/*
4493		 * Plugin is already at the latest version.
4494		 *
4495		 * This may also be the return value if the `update_plugins` site transient is empty,
4496		 * e.g. when you update two plugins in quick succession before the transient repopulates.
4497		 *
4498		 * Preferably something can be done to ensure `update_plugins` isn't empty.
4499		 * For now, surface some sort of error here.
4500		 */
4501		if ( true === $result[ $plugin ] ) {
4502			$status['errorMessage'] = $upgrader->strings['up_to_date'];
4503			wp_send_json_error( $status );
4504		}
4505
4506		$plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] );
4507		$plugin_data = reset( $plugin_data );
4508
4509		if ( $plugin_data['Version'] ) {
4510			/* translators: %s: Plugin version. */
4511			$status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
4512		}
4513
4514		wp_send_json_success( $status );
4515	} elseif ( false === $result ) {
4516		global $wp_filesystem;
4517
4518		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4519		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4520
4521		// Pass through the error from WP_Filesystem if one was raised.
4522		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4523			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4524		}
4525
4526		wp_send_json_error( $status );
4527	}
4528
4529	// An unhandled error occurred.
4530	$status['errorMessage'] = __( 'Plugin update failed.' );
4531	wp_send_json_error( $status );
4532}
4533
4534/**
4535 * Ajax handler for deleting a plugin.
4536 *
4537 * @since 4.6.0
4538 *
4539 * @see delete_plugins()
4540 *
4541 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
4542 */
4543function wp_ajax_delete_plugin() {
4544	check_ajax_referer( 'updates' );
4545
4546	if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
4547		wp_send_json_error(
4548			array(
4549				'slug'         => '',
4550				'errorCode'    => 'no_plugin_specified',
4551				'errorMessage' => __( 'No plugin specified.' ),
4552			)
4553		);
4554	}
4555
4556	$plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
4557
4558	$status = array(
4559		'delete' => 'plugin',
4560		'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
4561	);
4562
4563	if ( ! current_user_can( 'delete_plugins' ) || 0 !== validate_file( $plugin ) ) {
4564		$status['errorMessage'] = __( 'Sorry, you are not allowed to delete plugins for this site.' );
4565		wp_send_json_error( $status );
4566	}
4567
4568	$plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
4569	$status['plugin']     = $plugin;
4570	$status['pluginName'] = $plugin_data['Name'];
4571
4572	if ( is_plugin_active( $plugin ) ) {
4573		$status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' );
4574		wp_send_json_error( $status );
4575	}
4576
4577	// Check filesystem credentials. `delete_plugins()` will bail otherwise.
4578	$url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' );
4579
4580	ob_start();
4581	$credentials = request_filesystem_credentials( $url );
4582	ob_end_clean();
4583
4584	if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
4585		global $wp_filesystem;
4586
4587		$status['errorCode']    = 'unable_to_connect_to_filesystem';
4588		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
4589
4590		// Pass through the error from WP_Filesystem if one was raised.
4591		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
4592			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
4593		}
4594
4595		wp_send_json_error( $status );
4596	}
4597
4598	$result = delete_plugins( array( $plugin ) );
4599
4600	if ( is_wp_error( $result ) ) {
4601		$status['errorMessage'] = $result->get_error_message();
4602		wp_send_json_error( $status );
4603	} elseif ( false === $result ) {
4604		$status['errorMessage'] = __( 'Plugin could not be deleted.' );
4605		wp_send_json_error( $status );
4606	}
4607
4608	wp_send_json_success( $status );
4609}
4610
4611/**
4612 * Ajax handler for searching plugins.
4613 *
4614 * @since 4.6.0
4615 *
4616 * @global string $s Search term.
4617 */
4618function wp_ajax_search_plugins() {
4619	check_ajax_referer( 'updates' );
4620
4621	// Ensure after_plugin_row_{$plugin_file} gets hooked.
4622	wp_plugin_update_rows();
4623
4624	$pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4625	if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) {
4626		set_current_screen( $pagenow );
4627	}
4628
4629	/** @var WP_Plugins_List_Table $wp_list_table */
4630	$wp_list_table = _get_list_table(
4631		'WP_Plugins_List_Table',
4632		array(
4633			'screen' => get_current_screen(),
4634		)
4635	);
4636
4637	$status = array();
4638
4639	if ( ! $wp_list_table->ajax_user_can() ) {
4640		$status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
4641		wp_send_json_error( $status );
4642	}
4643
4644	// Set the correct requester, so pagination works.
4645	$_SERVER['REQUEST_URI'] = add_query_arg(
4646		array_diff_key(
4647			$_POST,
4648			array(
4649				'_ajax_nonce' => null,
4650				'action'      => null,
4651			)
4652		),
4653		network_admin_url( 'plugins.php', 'relative' )
4654	);
4655
4656	$GLOBALS['s'] = wp_unslash( $_POST['s'] );
4657
4658	$wp_list_table->prepare_items();
4659
4660	ob_start();
4661	$wp_list_table->display();
4662	$status['count'] = count( $wp_list_table->items );
4663	$status['items'] = ob_get_clean();
4664
4665	wp_send_json_success( $status );
4666}
4667
4668/**
4669 * Ajax handler for searching plugins to install.
4670 *
4671 * @since 4.6.0
4672 */
4673function wp_ajax_search_install_plugins() {
4674	check_ajax_referer( 'updates' );
4675
4676	$pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
4677	if ( 'plugin-install-network' === $pagenow || 'plugin-install' === $pagenow ) {
4678		set_current_screen( $pagenow );
4679	}
4680
4681	/** @var WP_Plugin_Install_List_Table $wp_list_table */
4682	$wp_list_table = _get_list_table(
4683		'WP_Plugin_Install_List_Table',
4684		array(
4685			'screen' => get_current_screen(),
4686		)
4687	);
4688
4689	$status = array();
4690
4691	if ( ! $wp_list_table->ajax_user_can() ) {
4692		$status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
4693		wp_send_json_error( $status );
4694	}
4695
4696	// Set the correct requester, so pagination works.
4697	$_SERVER['REQUEST_URI'] = add_query_arg(
4698		array_diff_key(
4699			$_POST,
4700			array(
4701				'_ajax_nonce' => null,
4702				'action'      => null,
4703			)
4704		),
4705		network_admin_url( 'plugin-install.php', 'relative' )
4706	);
4707
4708	$wp_list_table->prepare_items();
4709
4710	ob_start();
4711	$wp_list_table->display();
4712	$status['count'] = (int) $wp_list_table->get_pagination_arg( 'total_items' );
4713	$status['items'] = ob_get_clean();
4714
4715	wp_send_json_success( $status );
4716}
4717
4718/**
4719 * Ajax handler for editing a theme or plugin file.
4720 *
4721 * @since 4.9.0
4722 *
4723 * @see wp_edit_theme_plugin_file()
4724 */
4725function wp_ajax_edit_theme_plugin_file() {
4726	$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file().
4727
4728	if ( is_wp_error( $r ) ) {
4729		wp_send_json_error(
4730			array_merge(
4731				array(
4732					'code'    => $r->get_error_code(),
4733					'message' => $r->get_error_message(),
4734				),
4735				(array) $r->get_error_data()
4736			)
4737		);
4738	} else {
4739		wp_send_json_success(
4740			array(
4741				'message' => __( 'File edited successfully.' ),
4742			)
4743		);
4744	}
4745}
4746
4747/**
4748 * Ajax handler for exporting a user's personal data.
4749 *
4750 * @since 4.9.6
4751 */
4752function wp_ajax_wp_privacy_export_personal_data() {
4753
4754	if ( empty( $_POST['id'] ) ) {
4755		wp_send_json_error( __( 'Missing request ID.' ) );
4756	}
4757
4758	$request_id = (int) $_POST['id'];
4759
4760	if ( $request_id < 1 ) {
4761		wp_send_json_error( __( 'Invalid request ID.' ) );
4762	}
4763
4764	if ( ! current_user_can( 'export_others_personal_data' ) ) {
4765		wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
4766	}
4767
4768	check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );
4769
4770	// Get the request.
4771	$request = wp_get_user_request( $request_id );
4772
4773	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
4774		wp_send_json_error( __( 'Invalid request type.' ) );
4775	}
4776
4777	$email_address = $request->email;
4778	if ( ! is_email( $email_address ) ) {
4779		wp_send_json_error( __( 'A valid email address must be given.' ) );
4780	}
4781
4782	if ( ! isset( $_POST['exporter'] ) ) {
4783		wp_send_json_error( __( 'Missing exporter index.' ) );
4784	}
4785
4786	$exporter_index = (int) $_POST['exporter'];
4787
4788	if ( ! isset( $_POST['page'] ) ) {
4789		wp_send_json_error( __( 'Missing page index.' ) );
4790	}
4791
4792	$page = (int) $_POST['page'];
4793
4794	$send_as_email = isset( $_POST['sendAsEmail'] ) ? ( 'true' === $_POST['sendAsEmail'] ) : false;
4795
4796	/**
4797	 * Filters the array of exporter callbacks.
4798	 *
4799	 * @since 4.9.6
4800	 *
4801	 * @param array $args {
4802	 *     An array of callable exporters of personal data. Default empty array.
4803	 *
4804	 *     @type array ...$0 {
4805	 *         Array of personal data exporters.
4806	 *
4807	 *         @type callable $callback               Callable exporter function that accepts an
4808	 *                                                email address and a page and returns an array
4809	 *                                                of name => value pairs of personal data.
4810	 *         @type string   $exporter_friendly_name Translated user facing friendly name for the
4811	 *                                                exporter.
4812	 *     }
4813	 * }
4814	 */
4815	$exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
4816
4817	if ( ! is_array( $exporters ) ) {
4818		wp_send_json_error( __( 'An exporter has improperly used the registration filter.' ) );
4819	}
4820
4821	// Do we have any registered exporters?
4822	if ( 0 < count( $exporters ) ) {
4823		if ( $exporter_index < 1 ) {
4824			wp_send_json_error( __( 'Exporter index cannot be negative.' ) );
4825		}
4826
4827		if ( $exporter_index > count( $exporters ) ) {
4828			wp_send_json_error( __( 'Exporter index is out of range.' ) );
4829		}
4830
4831		if ( $page < 1 ) {
4832			wp_send_json_error( __( 'Page index cannot be less than one.' ) );
4833		}
4834
4835		$exporter_keys = array_keys( $exporters );
4836		$exporter_key  = $exporter_keys[ $exporter_index - 1 ];
4837		$exporter      = $exporters[ $exporter_key ];
4838
4839		if ( ! is_array( $exporter ) ) {
4840			wp_send_json_error(
4841				/* translators: %s: Exporter array index. */
4842				sprintf( __( 'Expected an array describing the exporter at index %s.' ), $exporter_key )
4843			);
4844		}
4845
4846		if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
4847			wp_send_json_error(
4848				/* translators: %s: Exporter array index. */
4849				sprintf( __( 'Exporter array at index %s does not include a friendly name.' ), $exporter_key )
4850			);
4851		}
4852
4853		$exporter_friendly_name = $exporter['exporter_friendly_name'];
4854
4855		if ( ! array_key_exists( 'callback', $exporter ) ) {
4856			wp_send_json_error(
4857				/* translators: %s: Exporter friendly name. */
4858				sprintf( __( 'Exporter does not include a callback: %s.' ), esc_html( $exporter_friendly_name ) )
4859			);
4860		}
4861
4862		if ( ! is_callable( $exporter['callback'] ) ) {
4863			wp_send_json_error(
4864				/* translators: %s: Exporter friendly name. */
4865				sprintf( __( 'Exporter callback is not a valid callback: %s.' ), esc_html( $exporter_friendly_name ) )
4866			);
4867		}
4868
4869		$callback = $exporter['callback'];
4870		$response = call_user_func( $callback, $email_address, $page );
4871
4872		if ( is_wp_error( $response ) ) {
4873			wp_send_json_error( $response );
4874		}
4875
4876		if ( ! is_array( $response ) ) {
4877			wp_send_json_error(
4878				/* translators: %s: Exporter friendly name. */
4879				sprintf( __( 'Expected response as an array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
4880			);
4881		}
4882
4883		if ( ! array_key_exists( 'data', $response ) ) {
4884			wp_send_json_error(
4885				/* translators: %s: Exporter friendly name. */
4886				sprintf( __( 'Expected data in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
4887			);
4888		}
4889
4890		if ( ! is_array( $response['data'] ) ) {
4891			wp_send_json_error(
4892				/* translators: %s: Exporter friendly name. */
4893				sprintf( __( 'Expected data array in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
4894			);
4895		}
4896
4897		if ( ! array_key_exists( 'done', $response ) ) {
4898			wp_send_json_error(
4899				/* translators: %s: Exporter friendly name. */
4900				sprintf( __( 'Expected done (boolean) in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
4901			);
4902		}
4903	} else {
4904		// No exporters, so we're done.
4905		$exporter_key = '';
4906
4907		$response = array(
4908			'data' => array(),
4909			'done' => true,
4910		);
4911	}
4912
4913	/**
4914	 * Filters a page of personal data exporter data. Used to build the export report.
4915	 *
4916	 * Allows the export response to be consumed by destinations in addition to Ajax.
4917	 *
4918	 * @since 4.9.6
4919	 *
4920	 * @param array  $response        The personal data for the given exporter and page.
4921	 * @param int    $exporter_index  The index of the exporter that provided this data.
4922	 * @param string $email_address   The email address associated with this personal data.
4923	 * @param int    $page            The page for this response.
4924	 * @param int    $request_id      The privacy request post ID associated with this request.
4925	 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
4926	 * @param string $exporter_key    The key (slug) of the exporter that provided this data.
4927	 */
4928	$response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key );
4929
4930	if ( is_wp_error( $response ) ) {
4931		wp_send_json_error( $response );
4932	}
4933
4934	wp_send_json_success( $response );
4935}
4936
4937/**
4938 * Ajax handler for erasing personal data.
4939 *
4940 * @since 4.9.6
4941 */
4942function wp_ajax_wp_privacy_erase_personal_data() {
4943
4944	if ( empty( $_POST['id'] ) ) {
4945		wp_send_json_error( __( 'Missing request ID.' ) );
4946	}
4947
4948	$request_id = (int) $_POST['id'];
4949
4950	if ( $request_id < 1 ) {
4951		wp_send_json_error( __( 'Invalid request ID.' ) );
4952	}
4953
4954	// Both capabilities are required to avoid confusion, see `_wp_personal_data_removal_page()`.
4955	if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) {
4956		wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
4957	}
4958
4959	check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' );
4960
4961	// Get the request.
4962	$request = wp_get_user_request( $request_id );
4963
4964	if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
4965		wp_send_json_error( __( 'Invalid request type.' ) );
4966	}
4967
4968	$email_address = $request->email;
4969
4970	if ( ! is_email( $email_address ) ) {
4971		wp_send_json_error( __( 'Invalid email address in request.' ) );
4972	}
4973
4974	if ( ! isset( $_POST['eraser'] ) ) {
4975		wp_send_json_error( __( 'Missing eraser index.' ) );
4976	}
4977
4978	$eraser_index = (int) $_POST['eraser'];
4979
4980	if ( ! isset( $_POST['page'] ) ) {
4981		wp_send_json_error( __( 'Missing page index.' ) );
4982	}
4983
4984	$page = (int) $_POST['page'];
4985
4986	/**
4987	 * Filters the array of personal data eraser callbacks.
4988	 *
4989	 * @since 4.9.6
4990	 *
4991	 * @param array $args {
4992	 *     An array of callable erasers of personal data. Default empty array.
4993	 *
4994	 *     @type array ...$0 {
4995	 *         Array of personal data exporters.
4996	 *
4997	 *         @type callable $callback               Callable eraser that accepts an email address and
4998	 *                                                a page and returns an array with boolean values for
4999	 *                                                whether items were removed or retained and any messages
5000	 *                                                from the eraser, as well as if additional pages are
5001	 *                                                available.
5002	 *         @type string   $exporter_friendly_name Translated user facing friendly name for the eraser.
5003	 *     }
5004	 * }
5005	 */
5006	$erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );
5007
5008	// Do we have any registered erasers?
5009	if ( 0 < count( $erasers ) ) {
5010
5011		if ( $eraser_index < 1 ) {
5012			wp_send_json_error( __( 'Eraser index cannot be less than one.' ) );
5013		}
5014
5015		if ( $eraser_index > count( $erasers ) ) {
5016			wp_send_json_error( __( 'Eraser index is out of range.' ) );
5017		}
5018
5019		if ( $page < 1 ) {
5020			wp_send_json_error( __( 'Page index cannot be less than one.' ) );
5021		}
5022
5023		$eraser_keys = array_keys( $erasers );
5024		$eraser_key  = $eraser_keys[ $eraser_index - 1 ];
5025		$eraser      = $erasers[ $eraser_key ];
5026
5027		if ( ! is_array( $eraser ) ) {
5028			/* translators: %d: Eraser array index. */
5029			wp_send_json_error( sprintf( __( 'Expected an array describing the eraser at index %d.' ), $eraser_index ) );
5030		}
5031
5032		if ( ! array_key_exists( 'eraser_friendly_name', $eraser ) ) {
5033			/* translators: %d: Eraser array index. */
5034			wp_send_json_error( sprintf( __( 'Eraser array at index %d does not include a friendly name.' ), $eraser_index ) );
5035		}
5036
5037		$eraser_friendly_name = $eraser['eraser_friendly_name'];
5038
5039		if ( ! array_key_exists( 'callback', $eraser ) ) {
5040			wp_send_json_error(
5041				sprintf(
5042					/* translators: %s: Eraser friendly name. */
5043					__( 'Eraser does not include a callback: %s.' ),
5044					esc_html( $eraser_friendly_name )
5045				)
5046			);
5047		}
5048
5049		if ( ! is_callable( $eraser['callback'] ) ) {
5050			wp_send_json_error(
5051				sprintf(
5052					/* translators: %s: Eraser friendly name. */
5053					__( 'Eraser callback is not valid: %s.' ),
5054					esc_html( $eraser_friendly_name )
5055				)
5056			);
5057		}
5058
5059		$callback = $eraser['callback'];
5060		$response = call_user_func( $callback, $email_address, $page );
5061
5062		if ( is_wp_error( $response ) ) {
5063			wp_send_json_error( $response );
5064		}
5065
5066		if ( ! is_array( $response ) ) {
5067			wp_send_json_error(
5068				sprintf(
5069					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5070					__( 'Did not receive array from %1$s eraser (index %2$d).' ),
5071					esc_html( $eraser_friendly_name ),
5072					$eraser_index
5073				)
5074			);
5075		}
5076
5077		if ( ! array_key_exists( 'items_removed', $response ) ) {
5078			wp_send_json_error(
5079				sprintf(
5080					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5081					__( 'Expected items_removed key in response array from %1$s eraser (index %2$d).' ),
5082					esc_html( $eraser_friendly_name ),
5083					$eraser_index
5084				)
5085			);
5086		}
5087
5088		if ( ! array_key_exists( 'items_retained', $response ) ) {
5089			wp_send_json_error(
5090				sprintf(
5091					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5092					__( 'Expected items_retained key in response array from %1$s eraser (index %2$d).' ),
5093					esc_html( $eraser_friendly_name ),
5094					$eraser_index
5095				)
5096			);
5097		}
5098
5099		if ( ! array_key_exists( 'messages', $response ) ) {
5100			wp_send_json_error(
5101				sprintf(
5102					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5103					__( 'Expected messages key in response array from %1$s eraser (index %2$d).' ),
5104					esc_html( $eraser_friendly_name ),
5105					$eraser_index
5106				)
5107			);
5108		}
5109
5110		if ( ! is_array( $response['messages'] ) ) {
5111			wp_send_json_error(
5112				sprintf(
5113					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5114					__( 'Expected messages key to reference an array in response array from %1$s eraser (index %2$d).' ),
5115					esc_html( $eraser_friendly_name ),
5116					$eraser_index
5117				)
5118			);
5119		}
5120
5121		if ( ! array_key_exists( 'done', $response ) ) {
5122			wp_send_json_error(
5123				sprintf(
5124					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
5125					__( 'Expected done flag in response array from %1$s eraser (index %2$d).' ),
5126					esc_html( $eraser_friendly_name ),
5127					$eraser_index
5128				)
5129			);
5130		}
5131	} else {
5132		// No erasers, so we're done.
5133		$eraser_key = '';
5134
5135		$response = array(
5136			'items_removed'  => false,
5137			'items_retained' => false,
5138			'messages'       => array(),
5139			'done'           => true,
5140		);
5141	}
5142
5143	/**
5144	 * Filters a page of personal data eraser data.
5145	 *
5146	 * Allows the erasure response to be consumed by destinations in addition to Ajax.
5147	 *
5148	 * @since 4.9.6
5149	 *
5150	 * @param array  $response        The personal data for the given exporter and page.
5151	 * @param int    $eraser_index    The index of the eraser that provided this data.
5152	 * @param string $email_address   The email address associated with this personal data.
5153	 * @param int    $page            The page for this response.
5154	 * @param int    $request_id      The privacy request post ID associated with this request.
5155	 * @param string $eraser_key      The key (slug) of the eraser that provided this data.
5156	 */
5157	$response = apply_filters( 'wp_privacy_personal_data_erasure_page', $response, $eraser_index, $email_address, $page, $request_id, $eraser_key );
5158
5159	if ( is_wp_error( $response ) ) {
5160		wp_send_json_error( $response );
5161	}
5162
5163	wp_send_json_success( $response );
5164}
5165
5166/**
5167 * Ajax handler for site health checks on server communication.
5168 *
5169 * @since 5.2.0
5170 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_dotorg_communication()
5171 * @see WP_REST_Site_Health_Controller::test_dotorg_communication()
5172 */
5173function wp_ajax_health_check_dotorg_communication() {
5174	_doing_it_wrong(
5175		'wp_ajax_health_check_dotorg_communication',
5176		sprintf(
5177		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
5178			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5179			'wp_ajax_health_check_dotorg_communication',
5180			'WP_REST_Site_Health_Controller::test_dotorg_communication'
5181		),
5182		'5.6.0'
5183	);
5184
5185	check_ajax_referer( 'health-check-site-status' );
5186
5187	if ( ! current_user_can( 'view_site_health_checks' ) ) {
5188		wp_send_json_error();
5189	}
5190
5191	if ( ! class_exists( 'WP_Site_Health' ) ) {
5192		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5193	}
5194
5195	$site_health = WP_Site_Health::get_instance();
5196	wp_send_json_success( $site_health->get_test_dotorg_communication() );
5197}
5198
5199/**
5200 * Ajax handler for site health checks on background updates.
5201 *
5202 * @since 5.2.0
5203 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_background_updates()
5204 * @see WP_REST_Site_Health_Controller::test_background_updates()
5205 */
5206function wp_ajax_health_check_background_updates() {
5207	_doing_it_wrong(
5208		'wp_ajax_health_check_background_updates',
5209		sprintf(
5210		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
5211			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5212			'wp_ajax_health_check_background_updates',
5213			'WP_REST_Site_Health_Controller::test_background_updates'
5214		),
5215		'5.6.0'
5216	);
5217
5218	check_ajax_referer( 'health-check-site-status' );
5219
5220	if ( ! current_user_can( 'view_site_health_checks' ) ) {
5221		wp_send_json_error();
5222	}
5223
5224	if ( ! class_exists( 'WP_Site_Health' ) ) {
5225		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5226	}
5227
5228	$site_health = WP_Site_Health::get_instance();
5229	wp_send_json_success( $site_health->get_test_background_updates() );
5230}
5231
5232/**
5233 * Ajax handler for site health checks on loopback requests.
5234 *
5235 * @since 5.2.0
5236 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_loopback_requests()
5237 * @see WP_REST_Site_Health_Controller::test_loopback_requests()
5238 */
5239function wp_ajax_health_check_loopback_requests() {
5240	_doing_it_wrong(
5241		'wp_ajax_health_check_loopback_requests',
5242		sprintf(
5243		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
5244			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5245			'wp_ajax_health_check_loopback_requests',
5246			'WP_REST_Site_Health_Controller::test_loopback_requests'
5247		),
5248		'5.6.0'
5249	);
5250
5251	check_ajax_referer( 'health-check-site-status' );
5252
5253	if ( ! current_user_can( 'view_site_health_checks' ) ) {
5254		wp_send_json_error();
5255	}
5256
5257	if ( ! class_exists( 'WP_Site_Health' ) ) {
5258		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
5259	}
5260
5261	$site_health = WP_Site_Health::get_instance();
5262	wp_send_json_success( $site_health->get_test_loopback_requests() );
5263}
5264
5265/**
5266 * Ajax handler for site health check to update the result status.
5267 *
5268 * @since 5.2.0
5269 */
5270function wp_ajax_health_check_site_status_result() {
5271	check_ajax_referer( 'health-check-site-status-result' );
5272
5273	if ( ! current_user_can( 'view_site_health_checks' ) ) {
5274		wp_send_json_error();
5275	}
5276
5277	set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) );
5278
5279	wp_send_json_success();
5280}
5281
5282/**
5283 * Ajax handler for site health check to get directories and database sizes.
5284 *
5285 * @since 5.2.0
5286 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::get_directory_sizes()
5287 * @see WP_REST_Site_Health_Controller::get_directory_sizes()
5288 */
5289function wp_ajax_health_check_get_sizes() {
5290	_doing_it_wrong(
5291		'wp_ajax_health_check_get_sizes',
5292		sprintf(
5293		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
5294			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
5295			'wp_ajax_health_check_get_sizes',
5296			'WP_REST_Site_Health_Controller::get_directory_sizes'
5297		),
5298		'5.6.0'
5299	);
5300
5301	check_ajax_referer( 'health-check-site-status-result' );
5302
5303	if ( ! current_user_can( 'view_site_health_checks' ) || is_multisite() ) {
5304		wp_send_json_error();
5305	}
5306
5307	if ( ! class_exists( 'WP_Debug_Data' ) ) {
5308		require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
5309	}
5310
5311	$sizes_data = WP_Debug_Data::get_sizes();
5312	$all_sizes  = array( 'raw' => 0 );
5313
5314	foreach ( $sizes_data as $name => $value ) {
5315		$name = sanitize_text_field( $name );
5316		$data = array();
5317
5318		if ( isset( $value['size'] ) ) {
5319			if ( is_string( $value['size'] ) ) {
5320				$data['size'] = sanitize_text_field( $value['size'] );
5321			} else {
5322				$data['size'] = (int) $value['size'];
5323			}
5324		}
5325
5326		if ( isset( $value['debug'] ) ) {
5327			if ( is_string( $value['debug'] ) ) {
5328				$data['debug'] = sanitize_text_field( $value['debug'] );
5329			} else {
5330				$data['debug'] = (int) $value['debug'];
5331			}
5332		}
5333
5334		if ( ! empty( $value['raw'] ) ) {
5335			$data['raw'] = (int) $value['raw'];
5336		}
5337
5338		$all_sizes[ $name ] = $data;
5339	}
5340
5341	if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) {
5342		wp_send_json_error( $all_sizes );
5343	}
5344
5345	wp_send_json_success( $all_sizes );
5346}
5347
5348/**
5349 * Ajax handler to renew the REST API nonce.
5350 *
5351 * @since 5.3.0
5352 */
5353function wp_ajax_rest_nonce() {
5354	exit( wp_create_nonce( 'wp_rest' ) );
5355}
5356
5357/**
5358 * Ajax handler to enable or disable plugin and theme auto-updates.
5359 *
5360 * @since 5.5.0
5361 */
5362function wp_ajax_toggle_auto_updates() {
5363	check_ajax_referer( 'updates' );
5364
5365	if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) {
5366		wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) );
5367	}
5368
5369	$asset = sanitize_text_field( urldecode( $_POST['asset'] ) );
5370
5371	if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) {
5372		wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) );
5373	}
5374	$state = $_POST['state'];
5375
5376	if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) {
5377		wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
5378	}
5379	$type = $_POST['type'];
5380
5381	switch ( $type ) {
5382		case 'plugin':
5383			if ( ! current_user_can( 'update_plugins' ) ) {
5384				$error_message = __( 'Sorry, you are not allowed to modify plugins.' );
5385				wp_send_json_error( array( 'error' => $error_message ) );
5386			}
5387
5388			$option = 'auto_update_plugins';
5389			/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
5390			$all_items = apply_filters( 'all_plugins', get_plugins() );
5391			break;
5392		case 'theme':
5393			if ( ! current_user_can( 'update_themes' ) ) {
5394				$error_message = __( 'Sorry, you are not allowed to modify themes.' );
5395				wp_send_json_error( array( 'error' => $error_message ) );
5396			}
5397
5398			$option    = 'auto_update_themes';
5399			$all_items = wp_get_themes();
5400			break;
5401		default:
5402			wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
5403	}
5404
5405	if ( ! array_key_exists( $asset, $all_items ) ) {
5406		$error_message = __( 'Invalid data. The item does not exist.' );
5407		wp_send_json_error( array( 'error' => $error_message ) );
5408	}
5409
5410	$auto_updates = (array) get_site_option( $option, array() );
5411
5412	if ( 'disable' === $state ) {
5413		$auto_updates = array_diff( $auto_updates, array( $asset ) );
5414	} else {
5415		$auto_updates[] = $asset;
5416		$auto_updates   = array_unique( $auto_updates );
5417	}
5418
5419	// Remove items that have been deleted since the site option was last updated.
5420	$auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );
5421
5422	update_site_option( $option, $auto_updates );
5423
5424	wp_send_json_success();
5425}
5426
5427/**
5428 * Ajax handler sends a password reset link.
5429 *
5430 * @since 5.7.0
5431 */
5432function wp_ajax_send_password_reset() {
5433
5434	// Validate the nonce for this action.
5435	$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
5436	check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' );
5437
5438	// Verify user capabilities.
5439	if ( ! current_user_can( 'edit_user', $user_id ) ) {
5440		wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) );
5441	}
5442
5443	// Send the password reset link.
5444	$user    = get_userdata( $user_id );
5445	$results = retrieve_password( $user->user_login );
5446
5447	if ( true === $results ) {
5448		wp_send_json_success(
5449			/* translators: %s: User's display name. */
5450			sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name )
5451		);
5452	} else {
5453		wp_send_json_error( $results->get_error_message() );
5454	}
5455}
5456