PluginProbe ʕ •ᴥ•ʔ
Simple Page Ordering / trunk
Simple Page Ordering vtrunk
2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.8.0 trunk 0.8.4 0.9 0.9.1 0.9.5 0.9.6 1.0 2.0 2.1 2.1.1 2.1.2 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.3
simple-page-ordering / class-simple-page-ordering.php
simple-page-ordering Last commit date
10up-lib 2 years ago dist 1 month ago class-simple-page-ordering.php 1 month ago readme.txt 1 month ago simple-page-ordering.php 1 month ago
class-simple-page-ordering.php
914 lines
1 <?php
2
3 namespace SimplePageOrdering;
4
5 use stdClass;
6 use WP_Error;
7 use WP_Post;
8 use WP_REST_Response;
9 use WP_Query;
10
11 // Useful global constants.
12 define( 'SIMPLE_PAGE_ORDERING_VERSION', '2.8.0' );
13
14 if ( ! class_exists( 'Simple_Page_Ordering' ) ) :
15
16 /**
17 * Simple_Page_Ordering class
18 */
19 class Simple_Page_Ordering {
20
21 /**
22 * Handles initializing this class and returning the singleton instance after it's been cached.
23 *
24 * @return null|Simple_Page_Ordering
25 */
26 public static function get_instance() {
27 // Store the instance locally to avoid private static replication
28 static $instance = null;
29
30 if ( null === $instance ) {
31 $instance = new self();
32 self::add_actions();
33 }
34
35 return $instance;
36 }
37
38 /**
39 * An empty constructor
40 *
41 * Purposely do nothing here
42 */
43 public function __construct() {}
44
45 /**
46 * Handles registering hooks that initialize this plugin.
47 */
48 public static function add_actions() {
49 add_action( 'load-edit.php', array( __CLASS__, 'load_edit_screen' ) );
50 add_action( 'wp_ajax_simple_page_ordering', array( __CLASS__, 'ajax_simple_page_ordering' ) );
51 add_action( 'wp_ajax_reset_simple_page_ordering', array( __CLASS__, 'ajax_reset_simple_page_ordering' ) );
52 add_action( 'rest_api_init', array( __CLASS__, 'rest_api_init' ) );
53
54 // Custom edit page actions.
55 add_action( 'post_action_spo-move-under-grandparent', array( __CLASS__, 'handle_move_under_grandparent' ) );
56 add_action( 'post_action_spo-move-under-sibling', array( __CLASS__, 'handle_move_under_sibling' ) );
57 }
58
59 /**
60 * Move a post in/up the post parent tree.
61 *
62 * This is a custom action on the edit page to modify the post parent
63 * to be the child it's current grandparent post. If no grandparent
64 * exists, the post becomes a top level page.
65 *
66 * @param int $post_id The post ID.
67 */
68 public static function handle_move_under_grandparent( $post_id ) {
69 $post = get_post( $post_id );
70 if ( ! $post ) {
71 self::redirect_to_referer();
72 }
73
74 check_admin_referer( "simple-page-ordering-nonce-move-{$post->ID}", 'spo_nonce' );
75
76 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
77 wp_die( esc_html__( 'You are not allowed to edit this item.', 'simple-page-ordering' ) );
78 }
79
80 if ( 0 === $post->post_parent ) {
81 // Top level. Politely continue without doing anything.
82 self::redirect_to_referer();
83 }
84
85 $ancestors = get_post_ancestors( $post );
86
87 // If only one ancestor, set to top level page.
88 if ( 1 === count( $ancestors ) ) {
89 $parent_id = 0;
90 } else {
91 $parent_id = $ancestors[1];
92 }
93
94 // Update the post.
95 wp_update_post(
96 array(
97 'ID' => $post->ID,
98 'post_parent' => $parent_id,
99 )
100 );
101
102 self::redirect_to_referer();
103 }
104
105 /**
106 * Move a post out/down the post parent tree.
107 *
108 * This is a custom action on the edit page to modify the post parent
109 * to be the child of it's previous sibling post on the current post
110 * tree.
111 *
112 * @param int $post_id The post ID.
113 */
114 public static function handle_move_under_sibling( $post_id ) {
115 $post = get_post( $post_id );
116 if ( ! $post ) {
117 self::redirect_to_referer();
118 }
119
120 check_admin_referer( "simple-page-ordering-nonce-move-{$post->ID}", 'spo_nonce' );
121
122 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
123 wp_die( esc_html__( 'You are not allowed to edit this item.', 'simple-page-ordering' ) );
124 }
125
126 list( 'top_level_pages' => $top_level_pages, 'children_pages' => $children_pages ) = self::get_walked_pages( $post->post_type );
127
128 // Get the relevant siblings.
129 if ( 0 === $post->post_parent ) {
130 $siblings = $top_level_pages;
131 } else {
132 $siblings = $children_pages[ $post->post_parent ];
133 }
134
135 // Check if the post being moved is a top level page.
136 $filtered_siblings = wp_list_filter( $siblings, array( 'ID' => $post->ID ) );
137 if ( empty( $filtered_siblings ) ) {
138 // Something went wrong. Do nothing.
139 self::redirect_to_referer();
140 }
141
142 // Find the previous page in the sibling tree
143 $key = array_key_first( $filtered_siblings );
144 if ( 0 === $key ) {
145 // It's the first page. Do nothing.
146 self::redirect_to_referer();
147 }
148
149 $previous_page = $siblings[ $key - 1 ];
150 $previous_page_id = $previous_page->ID;
151
152 // Update the post with the previous page as the parent.
153 wp_update_post(
154 array(
155 'ID' => $post->ID,
156 'post_parent' => $previous_page_id,
157 )
158 );
159
160 self::redirect_to_referer();
161 }
162
163 /**
164 * Redirect the user after modifying the post parent.
165 */
166 public static function redirect_to_referer() {
167 global $post_type;
168
169 $send_back = wp_get_referer();
170 if ( ! $send_back ||
171 str_contains( $send_back, 'post.php' ) ||
172 str_contains( $send_back, 'post-new.php' ) ) {
173 if ( 'attachment' === $post_type ) {
174 $send_back = admin_url( 'upload.php' );
175 } else {
176 $send_back = admin_url( 'edit.php' );
177 if ( ! empty( $post_type ) ) {
178 $send_back = add_query_arg( 'post_type', $post_type, $send_back );
179 }
180 }
181 } else {
182 $send_back = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), $send_back );
183 }
184
185 wp_safe_redirect( $send_back );
186 exit;
187 }
188
189 /**
190 * Walk the pages and return top level and children pages.
191 *
192 * @param string $post_type Post type to walk.
193 *
194 * @return array {
195 * @type WP_Post[] $top_level_pages Top level pages.
196 * @type WP_Post[] $children_pages Children pages.
197 * }
198 */
199 public static function get_walked_pages( $post_type = 'page' ) {
200 global $wpdb;
201 $pages = get_pages(
202 array(
203 'sort_column' => 'menu_order title',
204 'post_type' => $post_type,
205 )
206 );
207
208 $top_level_pages = array();
209 $children_pages = array();
210 $bad_parents = array();
211
212 foreach ( $pages as $page ) {
213 // Catch and repair bad pages.
214 if ( $page->post_parent === $page->ID ) {
215 $page->post_parent = 0;
216 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $page->ID ) );
217 clean_post_cache( $page );
218 $bad_parents[] = $page->ID;
219 }
220
221 if ( $page->post_parent > 0 ) {
222 $children_pages[ $page->post_parent ][] = $page;
223 } else {
224 $top_level_pages[] = $page;
225 }
226 }
227 // Reprime post cache for bad parents.
228 _prime_post_caches( $bad_parents, false, false );
229
230 return array(
231 'top_level_pages' => $top_level_pages,
232 'children_pages' => $children_pages,
233 );
234 }
235
236 /**
237 * Loads the plugin textdomain
238 */
239 public static function load_textdomain() {
240 _deprecated_function( __METHOD__, '2.8.0' );
241 }
242
243 /**
244 * Determine whether given post type is sortable or not.
245 *
246 * @param string $post_type Post type to check.
247 *
248 * @return boolean
249 */
250 private static function is_post_type_sortable( $post_type = 'post' ) {
251 $sortable = ( post_type_supports( $post_type, 'page-attributes' ) || is_post_type_hierarchical( $post_type ) );
252
253 /**
254 * Change default ordering support for a post type.
255 *
256 * @since 2.0.0
257 *
258 * @param boolean $sortable Whether this post type is sortable or not.
259 * @param string $post_type The post type being checked.
260 */
261 return apply_filters( 'simple_page_ordering_is_sortable', $sortable, $post_type );
262 }
263
264 /**
265 * Load up page ordering scripts for the edit screen
266 */
267 public static function load_edit_screen() {
268 $screen = get_current_screen();
269 $post_type = $screen->post_type;
270
271 // is post type sortable?
272 $sortable = self::is_post_type_sortable( $post_type );
273 if ( ! $sortable ) {
274 return;
275 }
276
277 // does user have the right to manage these post objects?
278 if ( ! self::check_edit_others_caps( $post_type ) ) {
279 return;
280 }
281
282 // add view by menu order to views
283 add_filter(
284 'views_' . $screen->id,
285 array(
286 __CLASS__,
287 'sort_by_order_link',
288 )
289 );
290 add_action( 'pre_get_posts', array( __CLASS__, 'filter_query' ) );
291 add_action( 'wp', array( __CLASS__, 'wp' ) );
292 add_action( 'admin_head', array( __CLASS__, 'admin_head' ) );
293 add_action( 'page_row_actions', array( __CLASS__, 'page_row_actions' ), 10, 2 );
294 }
295
296 /**
297 * This is to enable pagination.
298 *
299 * @param WP_Query $query The WP_Query instance (passed by reference).
300 */
301 public static function filter_query( $query ) {
302 if ( ! $query->is_main_query() ) {
303 return;
304 }
305
306 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
307 $is_simple_page_ordering = isset( $_GET['id'] ) ? 'simple-page-ordering' === $_GET['id'] : false;
308
309 if ( ! $is_simple_page_ordering ) {
310 return;
311 }
312
313 $query->set( 'posts_per_page', -1 );
314 }
315
316 /**
317 * when we load up our posts query, if we're actually sorting by menu order, initialize sorting scripts
318 */
319 public static function wp() {
320 $orderby = get_query_var( 'orderby' );
321 $screen = get_current_screen();
322 $post_type = $screen->post_type ?? 'post';
323
324 if ( ( is_string( $orderby ) && 0 === strpos( $orderby, 'menu_order' ) ) || ( isset( $orderby['menu_order'] ) && 'ASC' === $orderby['menu_order'] ) ) {
325
326 $script_name = 'dist/js/simple-page-ordering.js';
327 $script_asset_path = plugin_dir_path( __FILE__ ) . 'dist/js/simple-page-ordering.asset.php';
328 $script_asset = file_exists( $script_asset_path )
329 ? require $script_asset_path
330 : false;
331
332 if ( false !== $script_asset ) {
333 $script_url = plugins_url( $script_name, __FILE__ );
334 wp_enqueue_script( 'simple-page-ordering', $script_url, $script_asset['dependencies'], $script_asset['version'], true );
335
336 wp_localize_script(
337 'simple-page-ordering',
338 'simple_page_ordering_localized_data',
339 array(
340 '_wpnonce' => wp_create_nonce( 'simple-page-ordering-nonce' ),
341 /* translators: %1$s is replaced with the post type name */
342 'confirmation_msg' => sprintf( esc_html__( 'Are you sure you want to reset the ordering of the "%1$s" post type?', 'simple-page-ordering' ), $post_type ),
343 )
344 );
345
346 wp_enqueue_style( 'simple-page-ordering', plugins_url( '/dist/css/simple-page-ordering.css', __FILE__ ), [], $script_asset['version'] );
347 } else {
348 add_action(
349 'admin_notices',
350 function () {
351 ?>
352 <div class="notice notice-warning is-dismissible">
353 <p><?php echo wp_kses_post( __( 'It looks like you are using a development copy of <strong>Simple Page Ordering</strong>. Please run <code>npm i; npm run build</code> to create assets.', 'simple-page-ordering' ) ); ?></p>
354 </div>
355 <?php
356 }
357 );
358 }
359 }
360 }
361
362 /**
363 * Add page ordering help to the help tab
364 */
365 public static function admin_head() {
366 $screen = get_current_screen();
367 $post_type = $screen->post_type ?? 'post';
368
369 $screen->add_help_tab(
370 array(
371 'id' => 'simple_page_ordering_help_tab',
372 'title' => esc_html__( 'Simple Page Ordering', 'simple-page-ordering' ),
373 'content' => sprintf(
374 '<p>%s</p><a href="#" id="simple-page-ordering-reset" data-posttype="%s">%s</a>',
375 esc_html__( 'To reposition an item, simply drag and drop the row by "clicking and holding" it anywhere (outside of the links and form controls) and moving it to its new position.', 'simple-page-ordering' ),
376 esc_attr( get_query_var( 'post_type' ) ),
377 /* translators: %1$s is replaced with the post type name */
378 sprintf( esc_html__( 'Reset %1$s order', 'simple-page-ordering' ), $post_type )
379 ),
380 )
381 );
382 }
383
384 /**
385 * Modify the row actions for hierarchical post types.
386 *
387 * This adds the actions to change the parent/child relationships.
388 *
389 * @param array $actions An array of row action links.
390 * @param WP_Post $post The post object.
391 */
392 public static function page_row_actions( $actions, $post ) {
393 $post = get_post( $post );
394 if ( ! $post ) {
395 return $actions;
396 }
397
398 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
399 return $actions;
400 }
401
402 /**
403 * Allow or disallow new row actions.
404 *
405 * @since 2.7.5
406 *
407 * @param boolean $should_add_actions Whether to add the new row actions.
408 * @param array $actions An array of row action links.
409 * @param WP_Post $post The post object.
410 */
411 $should_add_actions = apply_filters( 'simple_page_ordering_allow_row_actions', true, $actions, $post );
412 if ( ! $should_add_actions ) {
413 return $actions;
414 }
415
416 list( 'top_level_pages' => $top_level_pages, 'children_pages' => $children_pages ) = self::get_walked_pages( $post->post_type );
417
418 $edit_link = get_edit_post_link( $post->ID, 'raw' );
419 $move_under_grandparent_link = add_query_arg(
420 array(
421 'action' => 'spo-move-under-grandparent',
422 'spo_nonce' => wp_create_nonce( "simple-page-ordering-nonce-move-{$post->ID}" ),
423 'post_type' => $post->post_type,
424 ),
425 $edit_link
426 );
427 $move_under_sibling_link = add_query_arg(
428 array(
429 'action' => 'spo-move-under-sibling',
430 'spo_nonce' => wp_create_nonce( "simple-page-ordering-nonce-move-{$post->ID}" ),
431 'post_type' => $post->post_type,
432 ),
433 $edit_link
434 );
435
436 $parent_id = $post->post_parent;
437 if ( $parent_id ) {
438 $actions['spo-move-under-grandparent'] = sprintf(
439 '<a href="%s">%s</a>',
440 esc_url( $move_under_grandparent_link ),
441 sprintf(
442 /* translators: %s: parent page/post title */
443 __( 'Move out from under %s', 'simple-page-ordering' ),
444 get_the_title( $parent_id )
445 )
446 );
447 }
448
449 // Get the relevant siblings.
450 if ( 0 === $post->post_parent ) {
451 $siblings = $top_level_pages;
452 } else {
453 $siblings = $children_pages[ $post->post_parent ] ?? [];
454 }
455
456 // Assume no sibling.
457 $sibling = 0;
458 // Check if the post being moved is a top level page.
459 $filtered_siblings = wp_list_filter( $siblings, array( 'ID' => $post->ID ) );
460 if ( ! empty( $filtered_siblings ) ) {
461 // Find the previous page in the sibling tree
462 $key = array_key_first( $filtered_siblings );
463 if ( 0 === $key ) {
464 // It's the first page, can't do anything.
465 $sibling = 0;
466 } else {
467 $previous_page = $siblings[ $key - 1 ];
468 $sibling = $previous_page->ID;
469 }
470 }
471
472 if ( $sibling ) {
473 $actions['spo-move-under-sibling'] = sprintf(
474 '<a href="%s">%s</a>',
475 esc_url( $move_under_sibling_link ),
476 sprintf(
477 /* translators: %s: sibling page/post title */
478 __( 'Move under %s', 'simple-page-ordering' ),
479 get_the_title( $sibling )
480 )
481 );
482 }
483
484 return $actions;
485 }
486
487 /**
488 * Page ordering ajax callback
489 *
490 * @return void
491 */
492 public static function ajax_simple_page_ordering() {
493 // check and make sure we have what we need
494 if ( empty( $_POST['id'] ) || ( ! isset( $_POST['previd'] ) && ! isset( $_POST['nextid'] ) ) ) {
495 die( - 1 );
496 }
497
498 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ) : '';
499
500 if ( ! wp_verify_nonce( $nonce, 'simple-page-ordering-nonce' ) ) {
501 die( -1 );
502 }
503
504 $post_id = empty( $_POST['id'] ) ? false : (int) $_POST['id'];
505 $previd = empty( $_POST['previd'] ) ? false : (int) $_POST['previd'];
506 $nextid = empty( $_POST['nextid'] ) ? false : (int) $_POST['nextid'];
507 $start = empty( $_POST['start'] ) ? 1 : (int) $_POST['start'];
508 $excluded = empty( $_POST['excluded'] ) ? array( $_POST['id'] ) : array_filter( (array) json_decode( $_POST['excluded'] ), 'intval' );
509
510 // real post?
511 $post = empty( $post_id ) ? false : get_post( (int) $post_id );
512 if ( ! $post ) {
513 die( - 1 );
514 }
515
516 // does user have the right to manage these post objects?
517 if ( ! self::check_edit_others_caps( $post->post_type ) ) {
518 die( - 1 );
519 }
520
521 $result = self::page_ordering( $post_id, $previd, $nextid, $start, $excluded );
522
523 if ( is_wp_error( $result ) ) {
524 die( -1 );
525 }
526
527 die( wp_json_encode( $result ) );
528 }
529
530 /**
531 * Page ordering reset ajax callback
532 *
533 * @return void
534 */
535 public static function ajax_reset_simple_page_ordering() {
536 global $wpdb;
537
538 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ) : '';
539
540 if ( ! wp_verify_nonce( $nonce, 'simple-page-ordering-nonce' ) ) {
541 die( -1 );
542 }
543
544 // check and make sure we have what we need
545 $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : '';
546
547 if ( empty( $post_type ) ) {
548 die( -1 );
549 }
550
551 // does user have the right to manage these post objects?
552 if ( ! self::check_edit_others_caps( $post_type ) ) {
553 die( -1 );
554 }
555
556 // reset the order of all posts of given post type
557 $wpdb->update( 'wp_posts', array( 'menu_order' => 0 ), array( 'post_type' => $post_type ), array( '%d' ), array( '%s' ) );
558
559 die( 0 );
560 }
561
562 /**
563 * Page ordering function
564 *
565 * @param int $post_id The post ID.
566 * @param int $previd The previous post ID.
567 * @param int $nextid The next post ID.
568 * @param int $start The start index.
569 * @param array $excluded Array of post IDs.
570 *
571 * @return object|WP_Error|"children"
572 */
573 public static function page_ordering( $post_id, $previd, $nextid, $start, $excluded ) {
574 // real post?
575 $post = empty( $post_id ) ? false : get_post( (int) $post_id );
576 if ( ! $post ) {
577 return new WP_Error( 'invalid', __( 'Missing mandatory parameters.', 'simple-page-ordering' ) );
578 }
579
580 // Badly written plug-in hooks for save post can break things.
581 if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
582 error_reporting( 0 ); // phpcs:ignore
583 }
584
585 global $wp_version;
586
587 $previd = empty( $previd ) ? false : (int) $previd;
588 $nextid = empty( $nextid ) ? false : (int) $nextid;
589 $start = empty( $start ) ? 1 : (int) $start;
590 $excluded = empty( $excluded ) ? array( $post_id ) : array_filter( (array) $excluded, 'intval' );
591
592 $new_pos = array(); // store new positions for ajax
593 $return_data = new stdClass();
594
595 do_action( 'simple_page_ordering_pre_order_posts', $post, $start );
596
597 // attempt to get the intended parent... if either sibling has a matching parent ID, use that
598 $parent_id = $post->post_parent;
599 $next_post_parent = $nextid ? wp_get_post_parent_id( $nextid ) : false;
600
601 if ( $previd === $next_post_parent ) { // if the preceding post is the parent of the next post, move it inside
602 $parent_id = $next_post_parent;
603 } elseif ( $next_post_parent !== $parent_id ) { // otherwise, if the next post's parent isn't the same as our parent, we need to study
604 $prev_post_parent = $previd ? wp_get_post_parent_id( $previd ) : false;
605 if ( $prev_post_parent !== $parent_id ) { // if the previous post is not our parent now, make it so!
606 $parent_id = ( false !== $prev_post_parent ) ? $prev_post_parent : $next_post_parent;
607 }
608 }
609
610 // if the next post's parent isn't our parent, it might as well be false (irrelevant to our query)
611 if ( $next_post_parent !== $parent_id ) {
612 $nextid = false;
613 }
614
615 $max_sortable_posts = (int) apply_filters( 'simple_page_ordering_limit', 50 ); // should reliably be able to do about 50 at a time
616
617 if ( $max_sortable_posts < 5 ) { // don't be ridiculous!
618 $max_sortable_posts = 50;
619 }
620
621 // we need to handle all post stati, except trash (in case of custom stati)
622 $post_stati = get_post_stati(
623 array(
624 'show_in_admin_all_list' => true,
625 )
626 );
627
628 $siblings_query = array(
629 'depth' => 1,
630 'posts_per_page' => $max_sortable_posts,
631 'post_type' => $post->post_type,
632 'post_status' => $post_stati,
633 'post_parent' => $parent_id,
634 'post__not_in' => $excluded, // phpcs:ignore
635 'orderby' => array(
636 'menu_order' => 'ASC',
637 'title' => 'ASC',
638 ),
639 'update_post_term_cache' => false,
640 'update_post_meta_cache' => false,
641 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFiltersTrue
642 'ignore_sticky_posts' => true,
643 );
644
645 if ( version_compare( $wp_version, '4.0', '<' ) ) {
646 $siblings_query['orderby'] = 'menu_order title';
647 $siblings_query['order'] = 'ASC';
648 }
649
650 $siblings = new WP_Query( $siblings_query ); // fetch all the siblings (relative ordering)
651
652 // don't waste overhead of revisions on a menu order change (especially since they can't *all* be rolled back at once)
653 remove_action( 'post_updated', 'wp_save_post_revision' );
654
655 foreach ( $siblings->posts as $sibling ) :
656 // don't handle the actual post
657 if ( $sibling->ID === $post->ID ) {
658 continue;
659 }
660
661 // if this is the post that comes after our repositioned post, set our repositioned post position and increment menu order
662 if ( $nextid === $sibling->ID ) {
663 wp_update_post(
664 array(
665 'ID' => $post->ID,
666 'menu_order' => $start,
667 'post_parent' => $parent_id,
668 )
669 );
670
671 $ancestors = get_post_ancestors( $post->ID );
672 $new_pos[ $post->ID ] = array(
673 'menu_order' => $start,
674 'post_parent' => $parent_id,
675 'depth' => count( $ancestors ),
676 );
677
678 $start ++;
679 }
680
681 // if repositioned post has been set, and new items are already in the right order, we can stop
682 if ( isset( $new_pos[ $post->ID ] ) && $sibling->menu_order >= $start ) {
683 $return_data->next = false;
684 break;
685 }
686
687 // set the menu order of the current sibling and increment the menu order
688 if ( $sibling->menu_order !== $start ) {
689 wp_update_post(
690 array(
691 'ID' => $sibling->ID,
692 'menu_order' => $start,
693 )
694 );
695 }
696 $new_pos[ $sibling->ID ] = $start;
697 $start ++;
698
699 if ( ! $nextid && $previd === $sibling->ID ) {
700 wp_update_post(
701 array(
702 'ID' => $post->ID,
703 'menu_order' => $start,
704 'post_parent' => $parent_id,
705 )
706 );
707
708 $ancestors = get_post_ancestors( $post->ID );
709 $new_pos[ $post->ID ] = array(
710 'menu_order' => $start,
711 'post_parent' => $parent_id,
712 'depth' => count( $ancestors ),
713 );
714 $start ++;
715 }
716
717 endforeach;
718
719 // max per request
720 if ( ! isset( $return_data->next ) && $siblings->max_num_pages > 1 ) {
721 $return_data->next = array(
722 'id' => $post->ID,
723 'previd' => $previd,
724 'nextid' => $nextid,
725 'start' => $start,
726 'excluded' => array_merge( array_keys( $new_pos ), $excluded ),
727 );
728 } else {
729 $return_data->next = false;
730 }
731
732 do_action( 'simple_page_ordering_ordered_posts', $post, $new_pos );
733
734 if ( ! $return_data->next ) {
735 // if the moved post has children, we need to refresh the page (unless we're continuing)
736 $children = new WP_Query(
737 array(
738 'posts_per_page' => 1,
739 'post_type' => $post->post_type,
740 'post_status' => $post_stati,
741 'post_parent' => $post->ID,
742 'fields' => 'ids',
743 'update_post_term_cache' => false,
744 'update_post_meta_cache' => false,
745 'ignore_sticky' => true,
746 'no_found_rows' => true,
747 )
748 );
749
750 if ( $children->have_posts() ) {
751 return 'children';
752 }
753 }
754
755 $return_data->new_pos = $new_pos;
756
757 return $return_data;
758 }
759
760 /**
761 * Append a sort by order link to the post actions
762 *
763 * @param array $views An array of available list table views.
764 *
765 * @return array
766 */
767 public static function sort_by_order_link( $views ) {
768 $class = ( get_query_var( 'orderby' ) === 'menu_order title' ) ? 'current' : '';
769 $query_string = remove_query_arg( array( 'orderby', 'order' ) );
770 if ( ! is_post_type_hierarchical( get_post_type() ) ) {
771 $query_string = add_query_arg( 'orderby', 'menu_order title', $query_string );
772 $query_string = add_query_arg( 'order', 'asc', $query_string );
773 $query_string = add_query_arg( 'id', 'simple-page-ordering', $query_string );
774 }
775 $views['byorder'] = sprintf( '<a href="%s" class="%s">%s</a>', esc_url( $query_string ), $class, __( 'Sort by Order', 'simple-page-ordering' ) );
776
777 return $views;
778 }
779
780 /**
781 * Checks to see if the current user has the capability to "edit others" for a post type
782 *
783 * @param string $post_type Post type name
784 *
785 * @return bool True or false
786 */
787 private static function check_edit_others_caps( $post_type ) {
788 $post_type_object = get_post_type_object( $post_type );
789 $edit_others_cap = empty( $post_type_object ) ? 'edit_others_' . $post_type . 's' : $post_type_object->cap->edit_others_posts;
790
791 return apply_filters( 'simple_page_ordering_edit_rights', current_user_can( $edit_others_cap ), $post_type );
792 }
793
794 /**
795 * Registers the API endpoint for sorting from the REST endpoint
796 */
797 public static function rest_api_init() {
798 register_rest_route(
799 'simple-page-ordering/v1',
800 'page_ordering',
801 [
802 'methods' => 'POST',
803 'callback' => array( __CLASS__, 'rest_page_ordering' ),
804 'permission_callback' => array( __CLASS__, 'rest_page_ordering_permissions_check' ),
805 'args' => [
806 'id' => [
807 'description' => __( 'ID of item we want to sort', 'simple-page-ordering' ),
808 'required' => true,
809 'type' => 'integer',
810 'minimum' => 1,
811 ],
812 'previd' => [
813 'description' => __( 'ID of item we want to be previous to after sorting', 'simple-page-ordering' ),
814 'required' => true,
815 'type' => [ 'boolean', 'integer' ],
816 ],
817 'nextid' => [
818 'description' => __( 'ID of item we want to be next to after sorting', 'simple-page-ordering' ),
819 'required' => true,
820 'type' => [ 'boolean', 'integer' ],
821 ],
822 'start' => [
823 'default' => 1,
824 'description' => __( 'Index we start with when sorting', 'simple-page-ordering' ),
825 'required' => false,
826 'type' => 'integer',
827 ],
828 'exclude' => [
829 'default' => [],
830 'description' => __( 'Array of IDs we want to exclude', 'simple-page-ordering' ),
831 'required' => false,
832 'type' => 'array',
833 'items' => [
834 'type' => 'integer',
835 ],
836 ],
837 ],
838 ]
839 );
840 }
841
842 /**
843 * Check if a given request has access to reorder content.
844 *
845 * This check ensures the current user making the request has
846 * proper permissions to edit the item, that the post type
847 * is allowed in REST requests and the post type is sortable.
848 *
849 * @since 2.5.1
850 *
851 * @param WP_REST_Request $request Full data about the request.
852 * @return bool|WP_Error
853 */
854 public static function rest_page_ordering_permissions_check( \WP_REST_Request $request ) {
855 $post_id = $request->get_param( 'id' );
856
857 // Ensure we have a logged in user that can edit the item.
858 if ( ! current_user_can( 'edit_post', $post_id ) ) {
859 return false;
860 }
861
862 $post_type = get_post_type( $post_id );
863 $post_type_obj = get_post_type_object( $post_type );
864
865 // Ensure the post type is allowed in REST endpoints.
866 if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) {
867 return false;
868 }
869
870 // Ensure this post type is sortable.
871 if ( ! self::is_post_type_sortable( $post_type ) ) {
872 return new WP_Error( 'not_enabled', esc_html__( 'This post type is not sortable.', 'simple-page-ordering' ) );
873 }
874
875 return true;
876 }
877
878 /**
879 * Handle REST page sorting
880 *
881 * @param WP_REST_Request $request The REST request object.
882 */
883 public static function rest_page_ordering( \WP_REST_Request $request ) {
884 $post_id = empty( $request->get_param( 'id' ) ) ? false : (int) $request->get_param( 'id' );
885 $previd = empty( $request->get_param( 'previd' ) ) ? false : (int) $request->get_param( 'previd' );
886 $nextid = empty( $request->get_param( 'nextid' ) ) ? false : (int) $request->get_param( 'nextid' );
887 $start = empty( $request->get_param( 'start' ) ) ? 1 : (int) $request->get_param( 'start' );
888 $excluded = empty( $request->get_param( 'excluded' ) ) ? array( $request->get_param( 'id' ) ) : array_filter( (array) json_decode( $request->get_param( 'excluded' ) ), 'intval' );
889
890 // Check and make sure we have what we need.
891 if ( false === $post_id || ( false === $previd && false === $nextid ) ) {
892 return new WP_Error( 'invalid', __( 'Missing mandatory parameters.', 'simple-page-ordering' ) );
893 }
894
895 $page_ordering = self::page_ordering( $post_id, $previd, $nextid, $start, $excluded );
896
897 if ( is_wp_error( $page_ordering ) ) {
898 return $page_ordering;
899 }
900
901 return new WP_REST_Response(
902 array(
903 'status' => 200,
904 'response' => 'success',
905 'body_response' => $page_ordering,
906 )
907 );
908 }
909 }
910
911 Simple_Page_Ordering::get_instance();
912
913 endif;
914