Beispiel #1
0
 static function fix($task)
 {
     $start = microtime(true);
     $total = $task->get("total");
     if (empty($total)) {
         // mptt: 2 operations for every item
         $total = 2 * db::build()->count_records("items");
         // album audit (permissions and bogus album covers): 1 operation for every album
         $total += db::build()->where("type", "=", "album")->count_records("items");
         // one operation for each missing slug, name and access cache
         foreach (array("find_dupe_slugs", "find_dupe_names", "find_missing_access_caches") as $func) {
             foreach (self::$func() as $row) {
                 $total++;
             }
         }
         $task->set("total", $total);
         $task->set("state", $state = self::FIX_STATE_START_MPTT);
         $task->set("ptr", 1);
         $task->set("completed", 0);
     }
     $completed = $task->get("completed");
     $state = $task->get("state");
     if (!module::get_var("gallery", "maintenance_mode")) {
         module::set_var("gallery", "maintenance_mode", 1);
     }
     // This is a state machine that checks each item in the database.  It verifies the following
     // attributes for an item.
     // 1. Left and right MPTT pointers are correct
     // 2. The .htaccess permission files for restricted items exist and are well formed.
     // 3. The relative_path_cache and relative_url_cache values are set to null.
     // 4. there are no album_cover_item_ids pointing to missing items
     //
     // We'll do a depth-first tree walk over our hierarchy using only the adjacency data because
     // we don't trust MPTT here (that might be what we're here to fix!).  Avoid avoid using ORM
     // calls as much as possible since they're expensive.
     //
     // NOTE: the MPTT check will only traverse items that have valid parents.  It's possible that
     // we have some tree corruption where there are items with parent ids to non-existent items.
     // We should probably do something about that.
     while ($state != self::FIX_STATE_DONE && microtime(true) - $start < 1.5) {
         switch ($state) {
             case self::FIX_STATE_START_MPTT:
                 $task->set("ptr", $ptr = 1);
                 $task->set("stack", item::root()->id . ":L");
                 $state = self::FIX_STATE_RUN_MPTT;
                 break;
             case self::FIX_STATE_RUN_MPTT:
                 $ptr = $task->get("ptr");
                 $stack = explode(" ", $task->get("stack"));
                 list($id, $ptr_mode) = explode(":", array_pop($stack));
                 if ($ptr_mode == "L") {
                     $stack[] = "{$id}:R";
                     db::build()->update("items")->set("left_ptr", $ptr++)->where("id", "=", $id)->execute();
                     foreach (db::build()->select(array("id"))->from("items")->where("parent_id", "=", $id)->order_by("left_ptr", "ASC")->execute() as $child) {
                         array_push($stack, "{$child->id}:L");
                     }
                 } else {
                     if ($ptr_mode == "R") {
                         db::build()->update("items")->set("right_ptr", $ptr++)->set("relative_path_cache", null)->set("relative_url_cache", null)->where("id", "=", $id)->execute();
                     }
                 }
                 $task->set("ptr", $ptr);
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_DUPE_SLUGS;
                 }
                 break;
             case self::FIX_STATE_START_DUPE_SLUGS:
                 $stack = array();
                 foreach (self::find_dupe_slugs() as $row) {
                     list($parent_id, $slug) = explode(":", $row->parent_slug, 2);
                     $stack[] = join(":", array($parent_id, $slug));
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_DUPE_SLUGS;
                 } else {
                     $state = self::FIX_STATE_START_DUPE_NAMES;
                 }
                 break;
             case self::FIX_STATE_RUN_DUPE_SLUGS:
                 $stack = explode(" ", $task->get("stack"));
                 list($parent_id, $slug) = explode(":", array_pop($stack));
                 // We want to leave the first one alone and update all conflicts to be random values.
                 $fixed = 0;
                 $conflicts = ORM::factory("item")->where("parent_id", "=", $parent_id)->where("slug", "=", $slug)->find_all(1, 1);
                 if ($conflicts->count() && ($conflict = $conflicts->current())) {
                     $task->log("Fixing conflicting slug for item id {$conflict->id}");
                     db::build()->update("items")->set("slug", $slug . "-" . (string) rand(1000, 9999))->where("id", "=", $conflict->id)->execute();
                     // We fixed one conflict, but there might be more so put this parent back on the stack
                     // and try again.  We won't consider it completed when we don't fix a conflict.  This
                     // guarantees that we won't spend too long fixing one set of conflicts, and that we
                     // won't stop before all are fixed.
                     $stack[] = "{$parent_id}:{$slug}";
                     break;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_DUPE_NAMES;
                 }
                 break;
             case self::FIX_STATE_START_DUPE_NAMES:
                 $stack = array();
                 foreach (self::find_dupe_names() as $row) {
                     list($parent_id, $name) = explode(":", $row->parent_name, 2);
                     $stack[] = join(":", array($parent_id, $name));
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_DUPE_NAMES;
                 } else {
                     $state = self::FIX_STATE_START_ALBUMS;
                 }
                 break;
             case self::FIX_STATE_RUN_DUPE_NAMES:
                 $stack = explode(" ", $task->get("stack"));
                 list($parent_id, $name) = explode(":", array_pop($stack));
                 $fixed = 0;
                 // We want to leave the first one alone and update all conflicts to be random values.
                 $conflicts = ORM::factory("item")->where("parent_id", "=", $parent_id)->where("name", "=", $name)->find_all(1, 1);
                 if ($conflicts->count() && ($conflict = $conflicts->current())) {
                     $task->log("Fixing conflicting name for item id {$conflict->id}");
                     db::build()->update("items")->set("name", $name . "-" . (string) rand(1000, 9999))->where("id", "=", $conflict->id)->execute();
                     // We fixed one conflict, but there might be more so put this parent back on the stack
                     // and try again.  We won't consider it completed when we don't fix a conflict.  This
                     // guarantees that we won't spend too long fixing one set of conflicts, and that we
                     // won't stop before all are fixed.
                     $stack[] = "{$parent_id}:{$name}";
                     break;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_ALBUMS;
                 }
                 break;
             case self::FIX_STATE_START_ALBUMS:
                 $stack = array();
                 foreach (db::build()->select("id")->from("items")->where("type", "=", "album")->execute() as $row) {
                     $stack[] = $row->id;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $state = self::FIX_STATE_RUN_ALBUMS;
                 break;
             case self::FIX_STATE_RUN_ALBUMS:
                 $stack = explode(" ", $task->get("stack"));
                 $id = array_pop($stack);
                 $item = ORM::factory("item", $id);
                 if ($item->album_cover_item_id) {
                     $album_cover_item = ORM::factory("item", $item->album_cover_item_id);
                     if (!$album_cover_item->loaded()) {
                         $item->album_cover_item_id = null;
                         $item->save();
                     }
                 }
                 $everybody = identity::everybody();
                 $view_col = "view_{$everybody->id}";
                 $view_full_col = "view_full_{$everybody->id}";
                 $intent = ORM::factory("access_intent")->where("item_id", "=", $id)->find();
                 if ($intent->{$view_col} === access::DENY) {
                     access::update_htaccess_files($item, $everybody, "view", access::DENY);
                 }
                 if ($intent->{$view_full_col} === access::DENY) {
                     access::update_htaccess_files($item, $everybody, "view_full", access::DENY);
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_MISSING_ACCESS_CACHES;
                 }
                 break;
             case self::FIX_STATE_START_MISSING_ACCESS_CACHES:
                 $stack = array();
                 foreach (self::find_missing_access_caches() as $row) {
                     $stack[] = $row->id;
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_MISSING_ACCESS_CACHES;
                 } else {
                     $state = self::FIX_STATE_DONE;
                 }
                 break;
             case self::FIX_STATE_RUN_MISSING_ACCESS_CACHES:
                 $stack = explode(" ", $task->get("stack"));
                 $id = array_pop($stack);
                 $access_cache = ORM::factory("access_cache");
                 $access_cache->item_id = $id;
                 $access_cache->save();
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     // The new cache rows are there, but they're incorrectly populated so we have to fix
                     // them.  If this turns out to be too slow, we'll have to refactor
                     // access::recalculate_permissions to allow us to do it in slices.
                     access::recalculate_album_permissions(item::root());
                     $state = self::FIX_STATE_DONE;
                 }
                 break;
         }
     }
     $task->set("state", $state);
     $task->set("completed", $completed);
     if ($state == self::FIX_STATE_DONE) {
         $task->done = true;
         $task->state = "success";
         $task->percent_complete = 100;
         module::set_var("gallery", "maintenance_mode", 0);
     } else {
         $task->percent_complete = round(100 * $completed / $total);
     }
     $task->status = t2("One operation complete", "%count / %total operations complete", $completed, array("total" => $total));
 }
Beispiel #2
0
 /**
  * Internal method to set a permission
  *
  * @param  Group_Model $group
  * @param  string      $perm_name
  * @param  Item_Model  $item
  * @param  boolean     $value
  */
 private static function _set(Group_Definition $group, $perm_name, $album, $value)
 {
     if (!$group instanceof Group_Definition) {
         throw new Exception("@todo PERMISSIONS_ONLY_WORK_ON_GROUPS");
     }
     if (!$album->loaded()) {
         throw new Exception("@todo INVALID_ALBUM {$album->id}");
     }
     if (!$album->is_album()) {
         throw new Exception("@todo INVALID_ALBUM_TYPE not an album");
     }
     $access = model_cache::get("access_intent", $album->id, "item_id");
     $access->__set("{$perm_name}_{$group->id}", $value);
     $access->save();
     if ($perm_name == "view") {
         self::_update_access_view_cache($group, $album);
     } else {
         self::_update_access_non_view_cache($group, $perm_name, $album);
     }
     access::update_htaccess_files($album, $group, $perm_name, $value);
     model_cache::clear();
 }
Beispiel #3
0
 static function fix($task)
 {
     $start = microtime(true);
     $total = $task->get("total");
     if (empty($total)) {
         $item_count = db::build()->count_records("items");
         $total = 0;
         // mptt: 2 operations for every item
         $total += 2 * $item_count;
         // album audit (permissions and bogus album covers): 1 operation for every album
         $total += db::build()->where("type", "=", "album")->count_records("items");
         // one operation for each dupe slug, dupe name, dupe base name, and missing access cache
         foreach (array("find_dupe_slugs", "find_dupe_names", "find_dupe_base_names", "find_missing_access_caches") as $func) {
             foreach (self::$func() as $row) {
                 $total++;
             }
         }
         // one operation to rebuild path and url caches;
         $total += 1 * $item_count;
         $task->set("total", $total);
         $task->set("state", $state = self::FIX_STATE_START_MPTT);
         $task->set("ptr", 1);
         $task->set("completed", 0);
     }
     $completed = $task->get("completed");
     $state = $task->get("state");
     if (!module::get_var("gallery", "maintenance_mode")) {
         module::set_var("gallery", "maintenance_mode", 1);
     }
     // This is a state machine that checks each item in the database.  It verifies the following
     // attributes for an item.
     // 1. Left and right MPTT pointers are correct
     // 2. The .htaccess permission files for restricted items exist and are well formed.
     // 3. The relative_path_cache and relative_url_cache values are set to null.
     // 4. there are no album_cover_item_ids pointing to missing items
     //
     // We'll do a depth-first tree walk over our hierarchy using only the adjacency data because
     // we don't trust MPTT here (that might be what we're here to fix!).  Avoid avoid using ORM
     // calls as much as possible since they're expensive.
     //
     // NOTE: the MPTT check will only traverse items that have valid parents.  It's possible that
     // we have some tree corruption where there are items with parent ids to non-existent items.
     // We should probably do something about that.
     while ($state != self::FIX_STATE_DONE && microtime(true) - $start < 1.5) {
         switch ($state) {
             case self::FIX_STATE_START_MPTT:
                 $task->set("ptr", $ptr = 1);
                 $task->set("stack", item::root()->id . "L1");
                 $state = self::FIX_STATE_RUN_MPTT;
                 break;
             case self::FIX_STATE_RUN_MPTT:
                 $ptr = $task->get("ptr");
                 $stack = explode(" ", $task->get("stack"));
                 preg_match("/([0-9]+)([A-Z])([0-9]+)/", array_pop($stack), $matches);
                 // e.g. "12345L10"
                 list(, $id, $ptr_mode, $level) = $matches;
                 // Skip the 0th entry of matches.
                 switch ($ptr_mode) {
                     case "L":
                         // Albums could be parent nodes.
                         $stack[] = "{$id}R{$level}";
                         db::build()->update("items")->set("left_ptr", $ptr++)->where("id", "=", $id)->execute();
                         $level++;
                         foreach (db::build()->select(array("id", "type"))->from("items")->where("parent_id", "=", $id)->order_by("left_ptr", "DESC")->execute() as $child) {
                             $stack[] = $child->type == "album" ? "{$child->id}L{$level}" : "{$child->id}B{$level}";
                         }
                         $completed++;
                         break;
                     case "B":
                         // Non-albums must be leaf nodes.
                         db::build()->update("items")->set("left_ptr", $ptr++)->set("right_ptr", $ptr++)->set("level", $level)->set("relative_path_cache", null)->set("relative_url_cache", null)->where("id", "=", $id)->execute();
                         $completed += 2;
                         // we updated two pointers
                         break;
                     case "R":
                         db::build()->update("items")->set("right_ptr", $ptr++)->set("level", $level)->set("relative_path_cache", null)->set("relative_url_cache", null)->where("id", "=", $id)->execute();
                         $completed++;
                 }
                 $task->set("ptr", $ptr);
                 $task->set("stack", implode(" ", $stack));
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_DUPE_SLUGS;
                 }
                 break;
             case self::FIX_STATE_START_DUPE_SLUGS:
                 $stack = array();
                 foreach (self::find_dupe_slugs() as $row) {
                     list($parent_id, $slug) = explode(":", $row->parent_slug, 2);
                     $stack[] = join(":", array($parent_id, $slug));
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_DUPE_SLUGS;
                 } else {
                     $state = self::FIX_STATE_START_DUPE_NAMES;
                 }
                 break;
             case self::FIX_STATE_RUN_DUPE_SLUGS:
                 $stack = explode(" ", $task->get("stack"));
                 list($parent_id, $slug) = explode(":", array_pop($stack));
                 // We want to leave the first one alone and update all conflicts to be random values.
                 $fixed = 0;
                 $conflicts = ORM::factory("item")->where("parent_id", "=", $parent_id)->where("slug", "=", $slug)->find_all(1, 1);
                 if ($conflicts->count() && ($conflict = $conflicts->current())) {
                     $task->log("Fixing conflicting slug for item id {$conflict->id}");
                     db::build()->update("items")->set("slug", $slug . "-" . (string) rand(1000, 9999))->where("id", "=", $conflict->id)->execute();
                     // We fixed one conflict, but there might be more so put this parent back on the stack
                     // and try again.  We won't consider it completed when we don't fix a conflict.  This
                     // guarantees that we won't spend too long fixing one set of conflicts, and that we
                     // won't stop before all are fixed.
                     $stack[] = "{$parent_id}:{$slug}";
                     break;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_DUPE_NAMES;
                 }
                 break;
             case self::FIX_STATE_START_DUPE_NAMES:
                 $stack = array();
                 foreach (self::find_dupe_names() as $row) {
                     list($parent_id, $name) = explode(":", $row->parent_name, 2);
                     $stack[] = join(":", array($parent_id, $name));
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_DUPE_NAMES;
                 } else {
                     $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
                 }
                 break;
             case self::FIX_STATE_RUN_DUPE_NAMES:
                 // NOTE: This does *not* attempt to fix the file system!
                 $stack = explode(" ", $task->get("stack"));
                 list($parent_id, $name) = explode(":", array_pop($stack));
                 $fixed = 0;
                 // We want to leave the first one alone and update all conflicts to be random values.
                 $conflicts = ORM::factory("item")->where("parent_id", "=", $parent_id)->where("name", "=", $name)->find_all(1, 1);
                 if ($conflicts->count() && ($conflict = $conflicts->current())) {
                     $task->log("Fixing conflicting name for item id {$conflict->id}");
                     if (!$conflict->is_album() && preg_match("/^(.*)(\\.[^\\.\\/]*?)\$/", $conflict->name, $matches)) {
                         $item_base_name = $matches[1];
                         $item_extension = $matches[2];
                         // includes a leading dot
                     } else {
                         $item_base_name = $conflict->name;
                         $item_extension = "";
                     }
                     db::build()->update("items")->set("name", $item_base_name . "-" . (string) rand(1000, 9999) . $item_extension)->where("id", "=", $conflict->id)->execute();
                     // We fixed one conflict, but there might be more so put this parent back on the stack
                     // and try again.  We won't consider it completed when we don't fix a conflict.  This
                     // guarantees that we won't spend too long fixing one set of conflicts, and that we
                     // won't stop before all are fixed.
                     $stack[] = "{$parent_id}:{$name}";
                     break;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
                 }
                 break;
             case self::FIX_STATE_START_DUPE_BASE_NAMES:
                 $stack = array();
                 foreach (self::find_dupe_base_names() as $row) {
                     list($parent_id, $base_name) = explode(":", $row->parent_base_name, 2);
                     $stack[] = join(":", array($parent_id, $base_name));
                 }
                 if ($stack) {
                     $task->set("stack", implode(" ", $stack));
                     $state = self::FIX_STATE_RUN_DUPE_BASE_NAMES;
                 } else {
                     $state = self::FIX_STATE_START_ALBUMS;
                 }
                 break;
             case self::FIX_STATE_RUN_DUPE_BASE_NAMES:
                 // NOTE: This *does* attempt to fix the file system!  So, it must go *after* run_dupe_names.
                 $stack = explode(" ", $task->get("stack"));
                 list($parent_id, $base_name) = explode(":", array_pop($stack));
                 $base_name_escaped = Database::escape_for_like($base_name);
                 $fixed = 0;
                 // We want to leave the first one alone and update all conflicts to be random values.
                 $conflicts = ORM::factory("item")->where("parent_id", "=", $parent_id)->where("name", "LIKE", "{$base_name_escaped}.%")->where("type", "<>", "album")->find_all(1, 1);
                 if ($conflicts->count() && ($conflict = $conflicts->current())) {
                     $task->log("Fixing conflicting name for item id {$conflict->id}");
                     if (preg_match("/^(.*)(\\.[^\\.\\/]*?)\$/", $conflict->name, $matches)) {
                         $item_base_name = $matches[1];
                         // unlike $base_name, this always maintains capitalization
                         $item_extension = $matches[2];
                         // includes a leading dot
                     } else {
                         $item_base_name = $conflict->name;
                         $item_extension = "";
                     }
                     // Unlike conflicts found in run_dupe_names, these items are likely to have an intact
                     // file system.  Let's use the item save logic to rebuild the paths and rename the files
                     // if possible.
                     try {
                         $conflict->name = $item_base_name . "-" . (string) rand(1000, 9999) . $item_extension;
                         $conflict->validate();
                         // If we get here, we're safe to proceed with save
                         $conflict->save();
                     } catch (Exception $e) {
                         // Didn't work.  Edit database directly without fixing file system.
                         db::build()->update("items")->set("name", $item_base_name . "-" . (string) rand(1000, 9999) . $item_extension)->where("id", "=", $conflict->id)->execute();
                     }
                     // We fixed one conflict, but there might be more so put this parent back on the stack
                     // and try again.  We won't consider it completed when we don't fix a conflict.  This
                     // guarantees that we won't spend too long fixing one set of conflicts, and that we
                     // won't stop before all are fixed.
                     $stack[] = "{$parent_id}:{$base_name}";
                     break;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_ALBUMS;
                 }
                 break;
             case self::FIX_STATE_START_ALBUMS:
                 $stack = array();
                 foreach (db::build()->select("id")->from("items")->where("type", "=", "album")->execute() as $row) {
                     $stack[] = $row->id;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $state = self::FIX_STATE_RUN_ALBUMS;
                 break;
             case self::FIX_STATE_RUN_ALBUMS:
                 $stack = explode(" ", $task->get("stack"));
                 $id = array_pop($stack);
                 $item = ORM::factory("item", $id);
                 if ($item->album_cover_item_id) {
                     $album_cover_item = ORM::factory("item", $item->album_cover_item_id);
                     if (!$album_cover_item->loaded()) {
                         $item->album_cover_item_id = null;
                         $item->save();
                     }
                 }
                 $everybody = identity::everybody();
                 $view_col = "view_{$everybody->id}";
                 $view_full_col = "view_full_{$everybody->id}";
                 $intent = ORM::factory("access_intent")->where("item_id", "=", $id)->find();
                 if ($intent->{$view_col} === access::DENY) {
                     access::update_htaccess_files($item, $everybody, "view", access::DENY);
                 }
                 if ($intent->{$view_full_col} === access::DENY) {
                     access::update_htaccess_files($item, $everybody, "view_full", access::DENY);
                 }
                 $task->set("stack", implode(" ", $stack));
                 $completed++;
                 if (empty($stack)) {
                     $state = self::FIX_STATE_START_REBUILD_ITEM_CACHES;
                 }
                 break;
             case self::FIX_STATE_START_REBUILD_ITEM_CACHES:
                 $stack = array();
                 foreach (self::find_empty_item_caches(500) as $row) {
                     $stack[] = $row->id;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $state = self::FIX_STATE_RUN_REBUILD_ITEM_CACHES;
                 break;
             case self::FIX_STATE_RUN_REBUILD_ITEM_CACHES:
                 $stack = explode(" ", $task->get("stack"));
                 if (!empty($stack)) {
                     $id = array_pop($stack);
                     $item = ORM::factory("item", $id);
                     $item->relative_path();
                     // this rebuilds the cache and saves the item as a side-effect
                     $task->set("stack", implode(" ", $stack));
                     $completed++;
                 }
                 if (empty($stack)) {
                     // Try refilling the stack
                     foreach (self::find_empty_item_caches(500) as $row) {
                         $stack[] = $row->id;
                     }
                     $task->set("stack", implode(" ", $stack));
                     if (empty($stack)) {
                         $state = self::FIX_STATE_START_MISSING_ACCESS_CACHES;
                     }
                 }
                 break;
             case self::FIX_STATE_START_MISSING_ACCESS_CACHES:
                 $stack = array();
                 foreach (self::find_missing_access_caches_limited(500) as $row) {
                     $stack[] = $row->id;
                 }
                 $task->set("stack", implode(" ", $stack));
                 $state = self::FIX_STATE_RUN_MISSING_ACCESS_CACHES;
                 break;
             case self::FIX_STATE_RUN_MISSING_ACCESS_CACHES:
                 $stack = array_filter(explode(" ", $task->get("stack")));
                 // filter removes empty/zero ids
                 if (!empty($stack)) {
                     $id = array_pop($stack);
                     $access_cache = ORM::factory("access_cache");
                     $access_cache->item_id = $id;
                     $access_cache->save();
                     $task->set("stack", implode(" ", $stack));
                     $completed++;
                 }
                 if (empty($stack)) {
                     // Try refilling the stack
                     foreach (self::find_missing_access_caches_limited(500) as $row) {
                         $stack[] = $row->id;
                     }
                     $task->set("stack", implode(" ", $stack));
                     if (empty($stack)) {
                         // The new cache rows are there, but they're incorrectly populated so we have to fix
                         // them.  If this turns out to be too slow, we'll have to refactor
                         // access::recalculate_permissions to allow us to do it in slices.
                         access::recalculate_album_permissions(item::root());
                         $state = self::FIX_STATE_DONE;
                     }
                 }
                 break;
         }
     }
     $task->set("state", $state);
     $task->set("completed", $completed);
     if ($state == self::FIX_STATE_DONE) {
         $task->done = true;
         $task->state = "success";
         $task->percent_complete = 100;
         module::set_var("gallery", "maintenance_mode", 0);
     } else {
         $task->percent_complete = round(100 * $completed / $total);
     }
     $task->status = t2("One operation complete", "%count / %total operations complete", $completed, array("total" => $total));
 }