Index: src/includes/admin/admin.php
===================================================================
--- src/includes/admin/admin.php	(revision 5892)
+++ src/includes/admin/admin.php	(working copy)
@@ -588,6 +588,10 @@
 					break;
 			}
 		}
+
+		if( 'tools_page_bbp-repair' === get_current_screen()->id ) {
+			wp_enqueue_script( 'bbp-tools-js', $this->js_url . 'tools.js', array( 'jquery' ), $version );
+		}
 	}
 
 	/**
Index: src/includes/admin/js/tools.js
===================================================================
--- src/includes/admin/js/tools.js	(revision 0)
+++ src/includes/admin/js/tools.js	(working copy)
@@ -0,0 +1,89 @@
+jQuery( document ).ready( function( ) {
+
+	// add in something here to check if the recalculate menu order box is ticked.
+	// If it is, it is the *only* one that can be ticked, maybe grey out the others?
+
+    var input = {};
+
+    // add run_sync to link
+    var sync_reply_positions =  jQuery( '#the-list a#bbp-sync-all-reply-positions' );
+    jQuery( sync_reply_positions ).addClass( 'run_sync' );
+
+    // disable checkbox for now
+    jQuery( '#the-list a#bbp-sync-all-reply-positions' ).closest( '#bbp-repair-tools' ).find( 'input[type=checkbox]' ).attr( 'disabled', 'disabled' );
+
+    function bbp_repair_reply_menu_order_call( nonce, iteration, continue_menu_order_zero  ) {
+
+        if( jQuery( '#the-list a#bbp-sync-all-reply-positions.killed' ).length > 0 ){
+            jQuery( '#the-list a#bbp-sync-all-reply-positions.killed' ).removeClass( 'killed' );
+            console.log( 'Process killed.' );
+            return;
+        }
+
+        input = {
+            'action'    : 'bbp_admin_repair_reply_menu_order',
+            'nonce'     : nonce,
+            'iteration' : iteration,
+            'continue_menu_order_zero' : continue_menu_order_zero
+        };
+
+        var bbp_repair_ajax = jQuery.ajax({
+                    type: 'POST',
+                    url : ajaxurl, // this is already there, let's use it
+                    data : input,
+                    dataType : 'json'
+               });
+
+        jQuery.when( bbp_repair_ajax ).then( function( data ){
+
+            if (data.result === 'keep-going' ) {
+
+                input = {};
+
+                if( jQuery( '#the-list a#bbp-sync-all-reply-positions.killed' ).length > 0 ){
+                    jQuery( '#the-list a#bbp-sync-all-reply-positions.killed' ).addClass( 'run_sync' );
+                }
+
+                console.log( data.notice );
+                return bbp_repair_reply_menu_order_call( data.passed_nonce, data.iteration, data.continue_menu_order_zero );
+            }
+            else if (data.result === 'all-done' ){
+                console.log( data.notice );
+                jQuery( '#the-list a#bbp-sync-all-reply-positions' ).removeClass( 'killed' ).removeClass( 'kill' ).html( 'Finished. Run again' );
+            }
+            else if (data.result === 'nonce-failed' ){
+                console.log( data.notice );
+            }
+            else{
+                console.log( 'boo' );
+            }
+        });
+    }
+
+    jQuery( '#the-list' ).on( 'click', 'a#bbp-sync-all-reply-positions.run_sync', function(e){
+
+        e.preventDefault();
+
+        // set the initial values
+        var nonce = jQuery( this ).attr( 'data-nonce' );
+        var iteration;
+        var continue_menu_order_zero = true;
+
+        // make the run link a kill link
+        jQuery( this ).removeClass( 'run_sync' ).addClass( 'kill' ).html( '<strong>Kill process (will take a few seconds)</strong>' );
+        jQuery( this ).closest( '.row-actions' ).css({ 'visibility' : 'visible' });
+
+        // and now call the function to start the repair
+        bbp_repair_reply_menu_order_call( nonce, iteration, continue_menu_order_zero );
+
+    });
+
+    jQuery( '#the-list' ).on( 'click', 'a#bbp-sync-all-reply-positions.kill', function(e){
+        e.preventDefault();
+
+        // don't kill anymore, we can run again
+        jQuery( this ).removeClass( 'kill' ).addClass( 'killed').html( 'Process killed. Click to restart.');
+    });
+
+
+});

Property changes on: src/includes/admin/js/tools.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/includes/admin/tools.php
===================================================================
--- src/includes/admin/tools.php	(revision 5892)
+++ src/includes/admin/tools.php	(working copy)
@@ -10,6 +10,9 @@
 // Exit if accessed directly
 defined( 'ABSPATH' ) || exit;
 
+// The reply menu order repair tool uses ajax
+add_action( 'wp_ajax_bbp_admin_repair_reply_menu_order', 'bbp_admin_repair_reply_menu_order_callback' );
+
 /** Repair ********************************************************************/
 
 /**
@@ -93,7 +96,7 @@
 									<strong><?php echo esc_html( $item['description'] ); ?></strong>
 									<div class="row-actions hide-if-no-js">
 										<span class="run">
-											<a href="<?php bbp_admin_repair_tool_run_url( $item['id'] ); ?>" aria-label="<?php printf( esc_html__( 'Run %s', 'bbpress' ), $item['description'] ); ?>" id="<?php echo esc_attr( $item['id'] ); ?>" ><?php esc_html_e( 'Run', 'bbpress' ); ?></a>
+											<a href="<?php bbp_admin_repair_tool_run_url( $item['id'] ); ?>" aria-label="<?php printf( esc_html__( 'Run %s', 'bbpress' ), $item['description'] ); ?>" id="<?php echo esc_attr( $item['id'] ); ?>" data-nonce="<?php echo wp_create_nonce( esc_attr( $item['id'] ) ); ?>" ><?php esc_html_e( 'Run', 'bbpress' ); ?></a>
 										</span>
 									</div>
 									<button type="button" class="toggle-row">
@@ -2074,8 +2077,23 @@
  * @uses bbp_update_reply_position() To update the reply position
  * @return array An array of the status code and the message
  */
-function bbp_admin_repair_reply_menu_order() {
+function bbp_admin_repair_reply_menu_order_callback() {
 
+	// nonceify this
+	$nonce = ( !empty( $_POST['nonce'] ) ) ? $_POST['nonce'] : '';
+	$iteration = ( !empty( $_POST['iteration'] ) ) ? intval( $_POST['iteration'] ) : 0;
+
+	$nonce_action = ( $iteration === 0 ) ? 'bbp-sync-all-reply-positions' : 'bbp-sync-all-reply-positions' . $iteration;
+
+	if( empty( wp_verify_nonce( $nonce, $nonce_action ) ) ){
+		$data = array('notice' => 'Nonce failed.' . $create_nonce . $nonce, 'continue_menu_order_zero' => '', 'result' => 'nonce-failed');
+ 		echo json_encode( $data );
+ 		wp_die();
+
+	}
+
+	$continue_menu_order_zero = ( !empty( $_POST['continue_menu_order_zero'] ) ) ?  $_POST['continue_menu_order_zero'] : false;
+
 	// Define variables
 	$bbp_db    = bbp_db();
 	$statement = __( 'Recalculating reply menu order &hellip; %s', 'bbpress' );
@@ -2089,11 +2107,120 @@
 	// Post type
 	$rpt = bbp_get_reply_post_type();
 
+	// Pending status
+ 	$pst = bbp_get_pending_status_id();
+ 	// Public status
+ 	$pub = bbp_get_public_status_id();
+
+ 	$offset = 0;
+ 	$limit = 300;
+ 	$replies_count = 0;
+
+ 	// ready steady GO
+ 	$startTime = microtime(true);
+
+ 	// First look through the menu_order 0 replies. Are there any published replies with menu_order 0?
+ 	// But don't do this if you don't need to.
+
+ 	$menu_order_zero_published = ( !$continue_menu_order_zero ) ? array() :
+ 		$bbp_db->get_results(
+ 			"SELECT `ID`
+ 				FROM `{$bbp_db->posts}`
+  				WHERE `post_type` = '{$rpt}' AND `post_status` = '{$pub}' AND `menu_order` = 0
+ 			 LIMIT {$limit}", OBJECT_K );
+
+ 	$menu_order_zero_count = count( $menu_order_zero_published );
+
+ 	// only get the array with duplicate menu orders if we're going to use it.
+ 	if( $menu_order_zero_count < $limit ){
+
+ 		$limit = $limit - $menu_order_zero_count;
+
+ 		// Get an array of reply ids to update the menu oder for each reply
+ 		$menu_order_repeated = $bbp_db->get_results(
+ 			"SELECT `a`.`ID` FROM `{$bbp_db->posts}` AS `a`
+ 				INNER JOIN (
+ 					SELECT `menu_order`, `post_parent`
+ 					FROM `{$bbp_db->posts}` WHERE `post_type` = '{$rpt}' AND `menu_order` != 0
+ 					GROUP BY `menu_order`, `post_parent`
+ 					HAVING COUNT( * ) >1
+ 				)`b`
+ 				ON `a`.`menu_order` = `b`.`menu_order`
+ 				AND `a`.`post_parent` = `b`.`post_parent`
+ 			 LIMIT {$limit}", OBJECT_K );
+
+ 		$replies = array_merge( $menu_order_zero_published, $menu_order_repeated );
+	} else{
+		$replies = $menu_order_zero_published;
+	}
+
+ 	$replies_count = count( $replies );
+
+ 	// Bail if no replies returned
+ 	if ( $replies_count === 0 ) {
+ 		// Flush the cache; things are about to get ugly.
+ 		$data = array('notice' => 'All done.', 'replies' => $replies, 'result' => 'all-done');
+ 		echo json_encode( $data );
+ 		wp_die();
+ 	}
+
+ 	$update_count = 0;
+
+ 	// Recalculate the menu order position for each reply
+ 	foreach ( $replies as $reply ) {
+ 		$data = bbp_update_reply_position( $reply->ID );
+ 		$update_count++;
+ 	}
+
+  	// Cleanup
+  	unset( $replies, $reply );
+ 	$endTime = microtime(true);
+
+ 	// If you did a limit's worth of updating replies and that amount is the same as menu_order=0 replies,
+ 	// then check again to see if there are any more. If not, don't do that query, they're all done.
+ 	$continue_menu_order_zero = ( $menu_order_zero_count === $update_count ) ? true : false;
+
+ 	// Let's make a new nonce for each pass through. Just for laughs.
+ 	$iteration++;
+ 	$new_nonce = wp_create_nonce( 'bbp-sync-all-reply-positions' . $iteration );
+
+ 	$data = array('notice' => 'still more to do, it has been ' .  number_format( $endTime - $startTime, 2 ) . ' seconds.', 'continue_menu_order_zero' => $continue_menu_order_zero, 'passed_nonce'=> $new_nonce, 'iteration' => $iteration, 'result' => 'keep-going');
+ 	echo json_encode( $data );
+ 	wp_die();
+
+ 	//return array( 0, sprintf( $statement, __('Average update time: ' . $averageUpdateTime . "\n\nStandard deviation: " . $stdDevUpdateTime . "\n\nAverage query time: " . $averageQueryTime . "\n\nOffset: " . $offset . ' // Replies count: ' . $replies_count . ' // Mid time: ' . ($midTime - $startTime) . ' // End time: ' . ($endTime - $startTime), 'bbpress' ) ) );
+  }
+
+/**
+ * Recalculate reply menu order
+ *
+ * @since bbPress (r5367)
+ *
+ * @uses wpdb::query() To run our recount sql queries
+ * @uses is_wp_error() To check if the executed query returned {@link WP_Error}
+ * @uses bbp_get_reply_post_type() To get the reply post type
+ * @uses bbp_update_reply_position() To update the reply position
+ * @return array An array of the status code and the message
+ */
+function bbp_admin_repair_reply_menu_order() {
+	global $wpdb;
+
+	$statement = __( 'Recalculating reply menu order &hellip; %s', 'bbpress' );
+	$result    = __( 'No reply positions to recalculate!',         'bbpress' );
+
+	// Delete cases where `_bbp_reply_to` was accidentally set to itself
+	if ( is_wp_error( $wpdb->query( "DELETE FROM `{$wpdb->postmeta}` WHERE `meta_key` = '_bbp_reply_to' AND `post_id` = `meta_value`;" ) ) ) {
+		return array( 1, sprintf( $statement, $result ) );
+	}
+
+	// Post type
+	$rpt = bbp_get_reply_post_type();
+
 	// Get an array of reply id's to update the menu oder for each reply
-	$replies = $bbp_db->get_results( "SELECT `a`.`ID` FROM `{$bbp_db->posts}` AS `a`
+	$replies = $wpdb->get_results( "SELECT `a`.`ID` FROM `{$wpdb->posts}` AS `a`
 										INNER JOIN (
 											SELECT `menu_order`, `post_parent`
-											FROM `{$bbp_db->posts}`
+											FROM `{$wpdb->posts}`
 											GROUP BY `menu_order`, `post_parent`
 											HAVING COUNT( * ) >1
 										)`b`
Index: src/includes/common/functions.php
===================================================================
--- src/includes/common/functions.php	(revision 5892)
+++ src/includes/common/functions.php	(working copy)
@@ -1612,7 +1612,7 @@
  *                        parent id and post type
  * @return array The array of children
  */
-function bbp_get_public_child_ids( $parent_id = 0, $post_type = 'post' ) {
+function bbp_get_public_child_ids( $parent_id = 0, $post_type = 'post', $order_by = 'ID', $use_cache = true ) {
 
 	// Bail if nothing passed
 	if ( empty( $parent_id ) ) {
@@ -1620,10 +1620,16 @@
 	}
 
 	// The ID of the cached query
-	$cache_id  = 'bbp_parent_public_' . $parent_id . '_type_' . $post_type . '_child_ids';
+	if( $order_by === 'ID' ){
+		$cache_id  = 'bbp_parent_public_' . $parent_id . '_type_' . $post_type . '_child_ids';
+	}
+	else{
+		$cache_id = 'bbp_' . $parent_id . '_public_children_orderby_' . $order_by;
+	}
 
 	// Check for cache and set if needed
-	$child_ids = wp_cache_get( $cache_id, 'bbpress_posts' );
+	$child_ids = ( $use_cache ) ? wp_cache_get( $cache_id, 'bbpress_posts' ) : false;
+
 	if ( false === $child_ids ) {
 		$post_status = array( bbp_get_public_status_id() );
 
@@ -1635,7 +1641,7 @@
 		// Join post statuses together
 		$post_status = "'" . implode( "', '", $post_status ) . "'";
 		$bbp_db      = bbp_db();
-		$query       = $bbp_db->prepare( "SELECT ID FROM {$bbp_db->posts} WHERE post_parent = %d AND post_status IN ( {$post_status} ) AND post_type = '%s' ORDER BY ID DESC;", $parent_id, $post_type );
+		$query       = $bbp_db->prepare( "SELECT ID FROM {$bbp_db->posts} WHERE post_parent = %d AND post_status IN ( {$post_status} ) AND post_type = '%s' ORDER BY {$order_by} DESC;", $parent_id, $post_type );
 		$child_ids   = (array) $bbp_db->get_col( $query );
 
 		wp_cache_set( $cache_id, $child_ids, 'bbpress_posts' );
@@ -1642,7 +1648,6 @@
 	} else {
 		$child_ids = (array) $child_ids;
 	}
-
 	// Filter and return
 	return (array) apply_filters( 'bbp_get_public_child_ids', $child_ids, $parent_id, $post_type );
 }
Index: src/includes/replies/functions.php
===================================================================
--- src/includes/replies/functions.php	(revision 5892)
+++ src/includes/replies/functions.php	(working copy)
@@ -2335,7 +2335,7 @@
 
 	// If no position was passed, get it from the db and update the menu_order
 	if ( empty( $reply_position ) ) {
-		$reply_position = bbp_get_reply_position_raw( $reply_id, bbp_get_reply_topic_id( $reply_id ) );
+		$reply_position = bbp_get_reply_position_published( $reply_id, bbp_get_reply_topic_id( $reply_id ) );
 	}
 
 	// Toggle revisions off as we are not altering content
@@ -2369,7 +2369,15 @@
  * @param int $topic_id
  */
 function bbp_get_reply_position_raw( $reply_id = 0, $topic_id = 0 ) {
+	return bbp_get_reply_position_any( $reply_id, $topic_id, $post_status = 'all' );
+}
 
+function bbp_get_reply_position_published( $reply_id = 0, $topic_id = 0 ) {
+	return bbp_get_reply_position_any( $reply_id, $topic_id, $post_status = 'publish' );
+}
+
+function bbp_get_reply_position_any( $reply_id = 0, $topic_id = 0, $post_status = 'all' ){
+
 	// Get required data
 	$reply_id       = bbp_get_reply_id( $reply_id );
 	$topic_id       = ! empty( $topic_id ) ? bbp_get_topic_id( $topic_id ) : bbp_get_reply_topic_id( $reply_id );
@@ -2383,7 +2391,14 @@
 		if ( ! empty( $reply_count ) ) {
 
 			// Get reply id's
-			$topic_replies = bbp_get_all_child_ids( $topic_id, bbp_get_reply_post_type() );
+			switch ( $post_status ) {
+				case 'publish' :
+					$topic_replies = bbp_get_public_child_ids( $topic_id, bbp_get_reply_post_type(), 'post_date DESC, ID', false );
+					break;
+				default:
+					$topic_replies = bbp_get_all_child_ids( $topic_id, bbp_get_reply_post_type() );
+			}
+
 			if ( ! empty( $topic_replies ) ) {
 
 				// Reverse replies array and search for current reply position
