monster_menus-9.0.x-dev/src/ValidateSortIndex.php
src/ValidateSortIndex.php
<?php
/**
* @file
* Service to validate and repair the MM tree.
*/
namespace Drupal\monster_menus;
use Drupal\Core\Database\Connection;
use Drupal\Core\GeneratedLink;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Render\Markup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drush\Drush;
use Psr\Log\LogLevel;
class ValidateSortIndex {
use StringTranslationTrait;
use MessengerTrait;
/**
* Send output to the current web page (do not use in a batch script). This
* is the default method, and is used by the admin/mm/sort menu entry.
*/
final public const OUTPUT_MODE_MESSAGE = 'message';
/**
* The \Drupal::logger() function (suitable for cron)
*/
final public const OUTPUT_MODE_WATCHDOG = 'watchdog';
/**
* Print the messages to standard i/o.
*/
final public const OUTPUT_MODE_PRINT = 'print';
/**
* Use when called by drush.
*/
final public const OUTPUT_MODE_DRUSH = 'drush';
/**
* Max. number of items to display
*/
final public const MM_ADMIN_VALIDATE_SORT_INDEX_MAX = 50;
/**
* The number of errors detected.
*
* @var int
*/
private $errors = 0;
/**
* The number of unfixable errors.
*
* @var int
*/
private $unfixableErrors = 0;
/**
* The current output mode.
*
* @var string
*/
private $outputMode = self::OUTPUT_MODE_MESSAGE;
/**
* Holds a list of tree segments to update upon completion.
*
* @var array
*/
private $sortQueueUpdates = [];
/**
* The database connection.
*
* @var Connection
*/
protected $database;
/**
* Constructs a ValidateSortIndex object.
*
* @param Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* Set the output mode.
*
* @param string $mode
* One of the OUTPUT_MODE_* constants.
* @return ValidateSortIndex
* The object, for chaining.
*/
public function setOutputMode($mode = self::OUTPUT_MODE_MESSAGE) {
$this->outputMode = $mode;
return $this;
}
/**
* Send a message to the output queue.
*
* @param bool $fix
* TRUE if errors are being fixed.
* @param string $message
* Message to log.
* @param array|null $subst
* Strings substituted into $message.
* @param bool $unfixable
* TRUE if the error cannot be fixed.
* @return mixed|void
*/
private function output($fix, $message, $subst, $unfixable = FALSE) {
if (is_null($message)) {
if ($this->outputMode == self::OUTPUT_MODE_MESSAGE) {
if ($this->errors) {
if ($fix) {
$fixable = $this->errors - $this->unfixableErrors;
$out = [];
if ($fixable) {
$out[] = \Drupal::translation()->formatPlural($fixable, 'One error was fixed.', '@count errors were fixed.');
}
if ($this->unfixableErrors) {
$out[] = \Drupal::translation()->formatPlural($this->unfixableErrors, 'There was one unfixable error.', 'There were @count unfixable errors.');
}
return implode(' ', $out);
}
if ($this->errors == $this->unfixableErrors) {
return $this->t('The listed error(s) cannot be fixed automatically.');
}
return [
'#type' => 'link',
'#title' => $this->t('Click here to fix all errors possible'),
'#url' => Url::fromRoute('monster_menus.admin_validate_sort_index_fix'),
];
}
return $this->t('No errors found.');
}
return;
}
if ($unfixable) {
$this->unfixableErrors++;
}
if ($this->outputMode == self::OUTPUT_MODE_WATCHDOG || $this->outputMode == self::OUTPUT_MODE_MESSAGE) {
if ($this->errors++ == self::MM_ADMIN_VALIDATE_SORT_INDEX_MAX) {
$message = 'Only the first @num messages are shown.';
$subst = ['@num' => self::MM_ADMIN_VALIDATE_SORT_INDEX_MAX];
}
elseif ($this->errors > self::MM_ADMIN_VALIDATE_SORT_INDEX_MAX) {
return;
}
}
else {
$this->errors++;
}
switch ($this->outputMode) {
case self::OUTPUT_MODE_WATCHDOG:
\Drupal::logger('mm')->error($message, $subst);
break;
case self::OUTPUT_MODE_MESSAGE:
if (!$fix) {
$this->messenger()->addError($this->t($message, $subst));
}
break;
case self::OUTPUT_MODE_PRINT:
print $this->t($message, $subst) . "\n";
break;
case self::OUTPUT_MODE_DRUSH:
Drush::service('logger')->log(LogLevel::NOTICE, $this->t($message, $subst));
break;
}
}
/**
* Queue a portion of the tree for a sort index update upon exit.
*
* @param int $mmtid
* The Tree ID of the portion to update.
* @param bool|null $all
* If TRUE, update all entries, not just the dirty ones.
* @param array $parent_mmtids
* A list of the parent Tree IDs.
*/
private function updateSortQueue($mmtid, $all, $parent_mmtids) {
if ($mmtid) {
// Make sure a particular entry doesn't already have parents that are
// going to be updated anyway. This only compares known parents, so
// store everything and do another pass at the end.
$parent = $mmtid;
while (isset($parent_mmtids[$parent]) && !isset($this->sortQueueUpdates[$parent])) {
$parent = $parent_mmtids[$parent];
}
if (!isset($this->sortQueueUpdates[$parent])) {
$this->sortQueueUpdates[$mmtid] = isset($this->sortQueueUpdates[$mmtid]) ? $this->sortQueueUpdates[$mmtid] | $all : $all;
}
}
else {
// Repeat the parent test for all entries flagged to be updated.
foreach ($this->sortQueueUpdates as $mmtid => $all) {
$parent = $mmtid;
while (isset($parent_mmtids[$parent]) && ($parent == $mmtid || !isset($this->sortQueueUpdates[$parent]))) {
$parent = $parent_mmtids[$parent];
}
if ($mmtid == 1 || !isset($this->sortQueueUpdates[$parent])) {
mm_content_update_sort_queue($mmtid, NULL, $all);
}
}
}
}
/**
* Return a link to a particular tree entry.
*
* @param $mmtid
* The Tree ID of the entry.
* @return GeneratedLink|string|int
* Various output, depending on the ::outputMode.
*/
private function mmtidLink($mmtid) {
if (strchr($mmtid, ',') !== FALSE) {
$out = [];
foreach (explode(',', $mmtid) as $m) {
$out[] = $this->mmtidLink($m);
}
$out = implode(', ', $out);
return $this->outputMode == self::OUTPUT_MODE_MESSAGE ? Markup::create($out) : $out;
}
if ($this->outputMode == self::OUTPUT_MODE_PRINT || $this->outputMode == self::OUTPUT_MODE_DRUSH) {
return $mmtid;
}
return Link::fromTextAndUrl($mmtid, mm_content_get_mmtid_url($mmtid))->toString();
}
private function moveToLostAndFound($mmtid, &$lost_found) {
if (self::createLostAndFound($lost_found, $error_message, $error_strings)) {
$this->output(FALSE, $error_message, $error_strings);
return TRUE;
}
$this->database->update('mm_tree')
->condition('mmtid', $mmtid)
->fields(['parent' => $lost_found, 'alias' => "RECOVER-$mmtid", 'sort_idx_dirty' => 1])
->execute();
mm_content_clear_caches($mmtid);
mm_content_update_parents($mmtid);
mm_content_update_sort_queue($lost_found);
return FALSE;
}
public static function createLostAndFound(&$mmtid, &$error_message, &$error_strings, $subpage = []) {
static $lost_found;
if (!isset($lost_found)) {
// Intentionally use MM_HOME_MMTID_DEFAULT instead of mm_home_mmtid(), since
// there might not be a '.System' page there.
$system = mm_content_get(['parent' => Constants::MM_HOME_MMTID_DEFAULT, 'name' => Constants::MM_ENTRY_NAME_SYSTEM]);
if (!$system) {
$error_message = 'Could not find @system page at the root level';
$error_strings = ['@system' => Constants::MM_ENTRY_NAME_SYSTEM];
return TRUE;
}
if ($tree = mm_content_get(['parent' => $system[0]->mmtid, 'name' => Constants::MM_ENTRY_NAME_LOST_FOUND])) {
$lost_found = $tree[0]->mmtid;
}
else {
try {
$lost_found = mm_content_insert_or_update(TRUE, $system[0]->mmtid, ['name' => Constants::MM_ENTRY_NAME_LOST_FOUND, 'alias' => Constants::MM_ENTRY_ALIAS_LOST_FOUND, 'hidden' => TRUE]);
}
catch (\Exception) {
}
if (empty($lost_found)) {
$error_message = 'Could not create the @lost page in @system';
$error_strings = ['@lost' => Constants::MM_ENTRY_NAME_LOST_FOUND, '@system' => Constants::MM_ENTRY_NAME_SYSTEM];
return TRUE;
}
}
}
$mmtid = $lost_found;
if ($lost_found && $subpage) {
if ($tree = mm_content_get(['parent' => $lost_found, 'alias' => $subpage['alias']])) {
$mmtid = $tree[0]->mmtid;
}
else {
try {
$mmtid = mm_content_insert_or_update(TRUE, $lost_found, $subpage);
}
catch (\Exception) {
}
if (empty($mmtid)) {
$error_message = 'Could not create the @lost subpage %alias';
$error_strings = ['@lost' => Constants::MM_ENTRY_NAME_LOST_FOUND, '%alias' => $subpage['alias']];
return TRUE;
}
}
}
return !$mmtid;
}
/**
* Check all mm_tree.sort_idx entries and optionally fix any that seem to be
* incorrect.
*
* @param bool $fix
* (optional) If TRUE, correct errors, when possible.
* @return string|void
* If the ::outputMode is ::OUTPUT_MODE_MESSAGE, a description of the
* number of errors, otherwise nothing.
*/
public function validate($fix = FALSE) {
// Wait up to 15 sec. to grab the semaphore.
if (($have_semaphore = mm_content_update_sort_test_semaphore(15)) !== TRUE) {
$this->output(FALSE, $have_semaphore->render(), []);
return $this->output(FALSE, NULL, NULL);
}
$lost_found = NULL;
$_mmtbt_cache = &drupal_static('_mmtbt_cache', []);
$q = $this->database->query('SELECT t.mmtid FROM {mm_tree} t LEFT JOIN {mm_tree_parents} p ON p.mmtid = t.mmtid AND p.parent = :mmtid1 WHERE t.mmtid <> :mmtid2 AND p.mmtid IS NULL', [':mmtid1' => 1, ':mmtid2' => 1]);
foreach ($q as $r) {
$msg = 'mmtid=@m is an orphan.';
$vars = ['@m' => $r->mmtid];
if ($fix) {
$parents = mm_content_get_parents($r->mmtid, TRUE, FALSE);
if ($parents && $parents[0] == 1) {
mm_content_update_parents($r->mmtid);
$msg = 'mmtid=@m was an orphan. It has been reattached to its parent.';
$vars['@m'] = $this->mmtidLink($r->mmtid);
}
else if (!$this->moveToLostAndFound($r->mmtid, $lost_found)) {
$msg = 'mmtid=@m was an orphan. It has been moved to the %lost page at mmtid=@lost';
$vars['%lost'] = mm_content_expand_name(Constants::MM_ENTRY_NAME_LOST_FOUND);
$vars['@lost'] = $this->mmtidLink($lost_found);
$vars['@m'] = $this->mmtidLink($r->mmtid);
}
}
$this->output($fix, $msg, $vars);
mm_content_update_sort_queue();
}
$parents = $parents_index = $parent_mmtids = $bins_to_empty = [];
$skip_bad = '';
$sibling = $prev = $parent = $lost_found = NULL;
$q = $this->database->query('SELECT t.*, (SELECT GROUP_CONCAT(p.parent ORDER BY p.depth) FROM {mm_tree_parents} p WHERE p.mmtid = t.mmtid) AS tree_parents FROM (SELECT :mmtid1 AS mmtid UNION SELECT p.mmtid FROM {mm_tree_parents} p WHERE p.parent = :mmtid2) x INNER JOIN {mm_tree} t ON t.mmtid = x.mmtid ORDER BY sort_idx', [':mmtid1' => 1, ':mmtid2' => 1]);
foreach ($q as $r) {
$skip_tests = FALSE;
if ($r->sort_idx_dirty) {
$skip_bad = $r->sort_idx;
$this->output($fix, $fix ? 'mmtid=@m (parent=@par) is marked as dirty. Updating now.' : 'mmtid=@m (parent=@par) is marked as dirty.', ['@m' => $this->mmtidLink($r->mmtid), '@par' => $this->mmtidLink($r->parent)]);
// An update was previously missed, so definitely do it now
$this->updateSortQueue($r->parent ?: 1, !$r->parent, $parent_mmtids);
}
if ($r->mmtid == 1) {
$parent = $prev = $r;
continue;
}
$parent_mmtids[$r->mmtid] = $r->parent;
$is_bad = FALSE;
if ($r->parent == $prev->mmtid) {
// Going deeper in tree
if (isset($parent)) {
$parents_index[] = $parent->mmtid;
$parent->sibling = $sibling;
$parents[] = $parent;
}
$parent = $prev;
$sibling = NULL;
// Note: There's no way to fix these next three, they're just warnings.
$q_temp = $this->database->query(
'SELECT mmtids, MAX(name) AS name, MAX(alias) AS alias FROM (' .
"SELECT GROUP_CONCAT(mmtid ORDER BY mmtid SEPARATOR ',') AS mmtids, name, '' AS alias " .
'FROM mm_tree WHERE parent = :parent GROUP BY name HAVING COUNT(*) > 1 ' .
'UNION ' .
"SELECT GROUP_CONCAT(mmtid ORDER BY mmtid SEPARATOR ',') AS mmtids, '' AS name, alias " .
"FROM mm_tree WHERE parent = :parent AND alias <> '' GROUP BY alias HAVING COUNT(*) > 1" .
') x GROUP BY mmtids', [':parent' => $parent->mmtid]);
foreach ($q_temp as $bad) {
if ($bad->name != '' && $bad->alias != '') {
$this->output($fix, 'Sibling entries share the same name/alias, @name/@alias: @mmtids.', ['@name' => $bad->name, '@alias' => $bad->alias, '@mmtids' => $this->mmtidLink($bad->mmtids)], TRUE);
}
else if ($bad->name != '') {
$this->output($fix, 'Sibling entries share the same name, @name: @mmtids.', ['@name' => $bad->name, '@mmtids' => $this->mmtidLink($bad->mmtids)], TRUE);
}
else {
$this->output($fix, 'Sibling entries share the same alias, @alias: @mmtids.', ['@alias' => $bad->alias, '@mmtids' => $this->mmtidLink($bad->mmtids)], TRUE);
}
}
$q_temp = $this->database->query("SELECT GROUP_CONCAT(mmtid ORDER BY mmtid SEPARATOR ',') AS mmtids, alias FROM {mm_tree} WHERE parent = :parent AND alias <> '' GROUP BY alias HAVING COUNT(*) > 1", [':parent' => $parent->mmtid]);
foreach ($q_temp as $bad) {
$this->output($fix, 'Sibling entries share the same alias, @alias: @mmtids.', ['@alias' => $bad->alias, '@mmtids' => $this->mmtidLink($bad->mmtids)], TRUE);
}
}
else {
if (strncmp($skip_bad, $r->sort_idx, strlen($skip_bad))) {
// We have gotten to a new parent or sibling of the previous bad item.
$skip_bad = '';
if ($sibling->parent != $r->parent) {
$sibling = NULL;
}
}
// See if we are going back up the tree
if (($which_parent = array_search($r->parent, $parents_index)) !== FALSE) {
$parent = $parents[$which_parent];
$parents_index = array_slice($parents_index, 0, $which_parent);
$parents = array_slice($parents, 0, $which_parent);
$sibling = $parent->sibling;
if ($sibling->parent != $r->parent) {
$sibling = NULL;
}
}
else if ($parent->mmtid != $r->parent) {
// If the item's parent exists, this means the sort index is off
if (mm_content_get($r->parent)) {
// The index is bad, but don't complain here; do it later on
$is_bad = TRUE;
}
else {
// The parent no longer exists, try to recover it.
$skip_tests = TRUE;
if (!$fix) {
$this->output(FALSE, 'Entry @mmtid has a missing parent :parent.', ['@mmtid' => $this->mmtidLink($r->mmtid), ':parent' => $r->parent]);
}
else if (!strncmp($r->tree_parents, '1,', 2)) {
$tree_parents = explode(',', $r->tree_parents);
$tree_parent = $tree_parents[count($tree_parents) - 1];
// Recoverable parent.
$this->database->update('mm_tree')
->condition('mmtid', $r->mmtid)
->fields(['parent' => $tree_parent, 'sort_idx_dirty' => 1])
->execute();
mm_content_clear_caches($r->mmtid);
$this->updateSortQueue($tree_parent, FALSE, $parent_mmtids);
$this->output(FALSE, 'Corrected parent of @mmtid.', ['@mmtid' => $this->mmtidLink($r->mmtid)]);
}
else if (!$this->moveToLostAndFound($r->mmtid, $lost_found)) {
$this->output(FALSE, 'Entry @mmtid had a missing parent, :parent. It has been moved to the %lost page at mmtid=@lost.', ['@mmtid' => $this->mmtidLink($r->mmtid), ':parent' => $r->parent, '%lost' => mm_content_expand_name(Constants::MM_ENTRY_NAME_LOST_FOUND), '@lost' => $this->mmtidLink($lost_found)]);
}
}
}
}
if (empty($skip_bad) && !$skip_tests) {
$parents_str = '';
$fixed_parents = [];
$m = $r->mmtid;
while ($m != 1) {
if (!isset($parent_mmtids[$m])) {
$temp_mmtid = mm_content_get_parent($m);
if (!is_numeric($temp_mmtid)) {
break;
}
$parent_mmtids[$m] = $temp_mmtid;
}
$m = $parent_mmtids[$m];
if (in_array($m, $fixed_parents)) {
// Should never happen, but this avoids a potential endless loop.
break;
}
$fixed_parents[] = $m;
$parents_str = $m . ($parents_str ? ',' : '') . $parents_str;
}
// Clear out the cache used by mm_content_get(), to save memory.
$_mmtbt_cache = [];
if ($m != 1) {
$this->output($fix, 'Entry at mmtid=@m has impossible parents [...:pars]. mm_tree_parents says they should be [:pars2]. This condition cannot be fixed automatically.', ['@m' => $this->mmtidLink($r->mmtid), ':pars' => $parents_str, ':pars2' => $r->tree_parents]);
$is_bad = TRUE;
}
elseif ($parents_str != $r->tree_parents) {
$this->output($fix, 'Entry at mmtid=@m has parents [:pars] which is inconsistent with mm_tree_parents [:pars2].', ['@m' => $this->mmtidLink($r->mmtid), ':pars' => $parents_str, ':pars2' => $r->tree_parents]);
if ($fix) mm_content_update_parents($r->mmtid, array_reverse($fixed_parents));
// Intentionally don't set $is_bad, since we don't want to skip kids
}
elseif ($r->parent != $parent->mmtid) {
$this->output($fix, 'Entry at mmtid=@m has a parent=@par that is inconsistent with its sort order.', ['@m' => $this->mmtidLink($r->mmtid), '@par' => $this->mmtidLink($r->parent)]);
}
elseif (strlen($r->sort_idx) - strlen($parent->sort_idx) != Constants::MM_CONTENT_BTOA_CHARS) {
// The length of this entry's index is not correct, relative to its parent
if (strlen($r->sort_idx) > strlen($parent->sort_idx)) {
$msg = 'Sort index at mmtid=@m (parent=@par) is too long.';
if (empty($skip_bad)) {
// Skip this item and its siblings because they all need to be fixed.
$skip_bad = substr($r->sort_idx, 0, strlen($parent->sort_idx) + Constants::MM_CONTENT_BTOA_CHARS);
}
}
else {
$msg = 'Sort index at mmtid=@m (parent=@par) is too short.';
}
$is_bad = TRUE;
$this->output($fix, $msg, ['@m' => $this->mmtidLink($r->mmtid), '@par' => $this->mmtidLink($r->parent)]);
}
else {
if ($r->sort_idx == $prev->sort_idx) {
// Entry has same index as its predecessor
$this->output($fix, 'mmtid=@m1 (parent=@par1) and mmtid=@m2 (parent=@par2) have the same sort index.', ['@m1' => $this->mmtidLink($r->mmtid), '@par1' => $this->mmtidLink($r->parent), '@m2' => $this->mmtidLink($prev->mmtid), '@par2' => $this->mmtidLink($prev->parent)]);
$is_bad = TRUE;
}
elseif (strncmp($r->sort_idx, $parent->sort_idx, strlen($parent->sort_idx))) {
// The indices should match, up to the parent's length
$this->output($fix, 'Sort indices of child and parent do not match at mmtid=@m (parent=@par).', ['@m' => $this->mmtidLink($r->mmtid), '@par' => $this->mmtidLink($r->parent)]);
$is_bad = TRUE;
}
elseif (isset($sibling)) {
if (substr($r->sort_idx, -Constants::MM_CONTENT_BTOA_CHARS) == substr($sibling->sort_idx, -Constants::MM_CONTENT_BTOA_CHARS)) {
// Entry has same index as its last sibling
$this->output($fix, 'Siblings mmtid=@m1 and mmtid=@m2 have the same sort index (their parent=@par).', ['@m1' => $this->mmtidLink($r->mmtid), '@m2' => $this->mmtidLink($sibling->mmtid), '@par' => $this->mmtidLink($r->parent)]);
$is_bad = TRUE;
}
else {
$msg = '';
if ($sibling->name == Constants::MM_ENTRY_NAME_RECYCLE && $r->name != Constants::MM_ENTRY_NAME_RECYCLE) {
$msg = 'Entry mmtid=@m1 comes after mmtid=@m2, which is a recycle bin (their parent=@par).';
}
elseif ($r->name != Constants::MM_ENTRY_NAME_RECYCLE && !$r->hidden) {
if ($sibling->hidden) {
$msg = 'Entry mmtid=@m1 is not hidden, but it comes after mmtid=@m2 which is (their parent=@par).';
}
elseif ($r->weight < $sibling->weight) {
$msg = 'Entry mmtid=@m1 has a weight that is lower than mmtid=@m2 (their parent=@par).';
}
elseif ($r->weight == $sibling->weight && strcasecmp($r->name, $sibling->name) < 0) {
// The simple cases are tested above, but strcasecmp() is not
// the same as collation-based comparisons in the DB, so upon
// failure, ask the DB if the sort is correct
$query = $this->database->select('mm_tree', 't1');
$query->join('mm_tree', 't2', 't2.parent = t1.parent');
$query->condition('t1.mmtid', $r->mmtid)
->condition('t2.mmtid', $sibling->mmtid)
->addExpression('t1.name < t2.name');
if ($query->execute()->fetchField()) {
$msg = 'Entry mmtid=@m1 comes after mmtid=@m2, even though it has a name that is earlier alphabetically (their parent=@par).';
}
}
}
if ($msg) {
$this->output($fix, $msg, ['@m1' => $this->mmtidLink($r->mmtid), '@m2' => $this->mmtidLink($sibling->mmtid), '@par' => $this->mmtidLink($r->parent)]);
$is_bad = TRUE;
}
}
}
}
}
if ($is_bad) {
if ($fix) {
$this->updateSortQueue($r->parent, TRUE, $parent_mmtids);
}
if (empty($skip_bad)) {
$skip_bad = $r->sort_idx;
}
}
else if ($r->name == Constants::MM_ENTRY_NAME_RECYCLE) {
if ($this->database->query('SELECT COUNT(*) FROM {mm_tree} t LEFT JOIN {mm_tree} t2 ON t2.parent = t.mmtid LEFT JOIN {mm_node2tree} n2 ON n2.mmtid = t.mmtid WHERE t.mmtid = :mmtid AND n2.mmtid IS NULL AND t2.mmtid IS NULL', [':mmtid' => $r->mmtid])->fetchField()) {
$this->output($fix, 'Recycle bin at mmtid=@m is empty.', ['@m' => $this->mmtidLink($r->mmtid)]);
if ($fix) {
$bins_to_empty[] = $r->mmtid;
}
}
else if (!$this->database->query('SELECT COUNT(*) FROM mm_recycle WHERE bin_mmtid = :mmtid', [':mmtid' => $r->mmtid])->fetchField()) {
$this->output(FALSE, 'Recycle bin at mmtid=@m has contents that are not properly recoverable. This condition cannot be fixed automatically. The best remedy may be to manually empty the bin.', ['@m' => $this->mmtidLink($r->mmtid)], TRUE);
}
}
$sibling = $prev = $r;
}
// Process the list of potential pages needing update one more time, and
// finally queue the pages for update.
$this->updateSortQueue(0, NULL, $parent_mmtids);
// Now process the updates. We can't use $semaphore_time here because we
// already have the semaphore.
mm_content_update_sort_queue();
// Remove any empty bins.
if ($bins_to_empty) {
foreach ($bins_to_empty as $bin) {
// This does one more check for emptiness.
mm_content_delete_bin($bin);
}
mm_content_update_sort_queue();
}
// And, finally, release the semaphore.
mm_content_update_sort_test_semaphore(-1);
return $this->output($fix, NULL, NULL);
}
}
