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