loaded()) {
      // Set reasonable defaults
      $this->created = time();
      $this->rand_key = random::percent();
      $this->thumb_dirty = 1;
      $this->resize_dirty = 1;
      $this->sort_column = "created";
      $this->sort_order = "ASC";
      $this->owner_id = identity::active_user()->id;
    }
  }
  /**
   * Add a set of restrictions to any following queries to restrict access only to items
   * viewable by the active user.
   * @chainable
   */
  public function viewable() {
    return item::viewable($this);
  }
  /**
   * Is this item an album?
   * @return true if it's an album
   */
  public function is_album() {
    return $this->type == 'album';
  }
  /**
   * Is this item a photo?
   * @return true if it's a photo
   */
  public function is_photo() {
    return $this->type == 'photo';
  }
  /**
   * Is this item a movie?
   * @return true if it's a movie
   */
  public function is_movie() {
    return $this->type == 'movie';
  }
  public function delete($ignored_id=null) {
    if (!$this->loaded()) {
      // Concurrent deletes may result in this item already being gone.  Ignore it.
      return;
    }
    if ($this->id == 1) {
      $v = new Validation(array("id"));
      $v->add_error("id", "cant_delete_root_album");
      ORM_Validation_Exception::handle_validation($this->table_name, $v);
    }
    $old = clone $this;
    module::event("item_before_delete", $this);
    $parent = $this->parent();
    if ($parent->album_cover_item_id == $this->id) {
      item::remove_album_cover($parent);
    }
    $path = $this->file_path();
    $resize_path = $this->resize_path();
    $thumb_path = $this->thumb_path();
    parent::delete();
    if (is_dir($path)) {
      // Take some precautions against accidentally deleting way too much
      $delete_resize_path = dirname($resize_path);
      $delete_thumb_path = dirname($thumb_path);
      if ($delete_resize_path == VARPATH . "resizes" ||
          $delete_thumb_path == VARPATH . "thumbs" ||
          $path == VARPATH . "albums") {
        throw new Exception(
          "@todo DELETING_TOO_MUCH ($delete_resize_path, $delete_thumb_path, $path)");
      }
      @dir::unlink($path);
      @dir::unlink($delete_resize_path);
      @dir::unlink($delete_thumb_path);
    } else {
      @unlink($path);
      @unlink($resize_path);
      @unlink($thumb_path);
    }
    module::event("item_deleted", $old);
  }
  /**
   * Specify the path to the data file associated with this item.  To actually associate it,
   * you still have to call save().
   * @chainable
   */
  public function set_data_file($data_file) {
    $this->data_file = $data_file;
    return $this;
  }
  /**
   * Return the server-relative url to this item, eg:
   *   /gallery3/index.php/BobsWedding?page=2
   *   /gallery3/index.php/BobsWedding/Eating-Cake.jpg
   *
   * @param string $query the query string (eg "show=3")
   */
  public function url($query=null) {
    $url = url::site($this->relative_url());
    if ($query) {
      $url .= "?$query";
    }
    return $url;
  }
  /**
   * Return the full url to this item, eg:
   *   http://example.com/gallery3/index.php/BobsWedding?page=2
   *   http://example.com/gallery3/index.php/BobsWedding/Eating-Cake.jpg
   *
   * @param string $query the query string (eg "show=3")
   */
  public function abs_url($query=null) {
    $url = url::abs_site($this->relative_url());
    if ($query) {
      $url .= "?$query";
    }
    return $url;
  }
  /**
   * album: /var/albums/album1/album2
   * photo: /var/albums/album1/album2/photo.jpg
   */
  public function file_path() {
    return VARPATH . "albums/" . urldecode($this->relative_path());
  }
  /**
   * album: http://example.com/gallery3/var/resizes/album1/
   * photo: http://example.com/gallery3/var/albums/album1/photo.jpg
   */
  public function file_url($full_uri=false) {
    $relative_path = "var/albums/" . $this->relative_path();
    $cache_buster = $this->_cache_buster($this->file_path());
    return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path))
      . $cache_buster;
  }
  /**
   * album: /var/resizes/album1/.thumb.jpg
   * photo: /var/albums/album1/photo.thumb.jpg
   */
  public function thumb_path() {
    $base = VARPATH . "thumbs/" . urldecode($this->relative_path());
    if ($this->is_photo()) {
      return $base;
    } else if ($this->is_album()) {
      return $base . "/.album.jpg";
    } else if ($this->is_movie()) {
      // Replace the extension with jpg
      return preg_replace("/...$/", "jpg", $base);
    }
  }
  /**
   * Return true if there is a thumbnail for this item.
   */
  public function has_thumb() {
    return $this->thumb_width && $this->thumb_height;
  }
  /**
   * album: http://example.com/gallery3/var/resizes/album1/.thumb.jpg
   * photo: http://example.com/gallery3/var/albums/album1/photo.thumb.jpg
   */
  public function thumb_url($full_uri=false) {
    $cache_buster = $this->_cache_buster($this->thumb_path());
    $relative_path = "var/thumbs/" . $this->relative_path();
    $base = ($full_uri ? url::abs_file($relative_path) : url::file($relative_path));
    if ($this->is_photo()) {
      return $base . $cache_buster;
    } else if ($this->is_album()) {
      return $base . "/.album.jpg" . $cache_buster;
    } else if ($this->is_movie()) {
      // Replace the extension with jpg
      $base = preg_replace("/...$/", "jpg", $base);
      return $base . $cache_buster;
    }
  }
  /**
   * album: /var/resizes/album1/.resize.jpg
   * photo: /var/albums/album1/photo.resize.jpg
   */
  public function resize_path() {
    return VARPATH . "resizes/" . urldecode($this->relative_path()) .
      ($this->is_album() ? "/.album.jpg" : "");
  }
  /**
   * album: http://example.com/gallery3/var/resizes/album1/.resize.jpg
   * photo: http://example.com/gallery3/var/albums/album1/photo.resize.jpg
   */
  public function resize_url($full_uri=false) {
    $relative_path = "var/resizes/" . $this->relative_path();
    $cache_buster = $this->_cache_buster($this->resize_path());
    return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path)) .
      ($this->is_album() ? "/.album.jpg" : "") . $cache_buster;
  }
  /**
   * Rebuild the relative_path_cache and relative_url_cache.
   */
  private function _build_relative_caches() {
    $names = array();
    $slugs = array();
    foreach (db::build()
             ->select(array("name", "slug"))
             ->from("items")
             ->where("left_ptr", "<=", $this->left_ptr)
             ->where("right_ptr", ">=", $this->right_ptr)
             ->where("id", "<>", 1)
             ->order_by("left_ptr", "ASC")
             ->execute() as $row) {
      // Don't encode the names segment
      $names[] = rawurlencode($row->name);
      $slugs[] = rawurlencode($row->slug);
    }
    $this->relative_path_cache = implode($names, "/");
    $this->relative_url_cache = implode($slugs, "/");
    return $this;
  }
  /**
   * Return the relative path to this item's file.  Note that the components of the path are
   * urlencoded so if you want to use this as a filesystem path, you need to call urldecode
   * on it.
   * @return string
   */
  public function relative_path() {
    if (!$this->loaded()) {
      return;
    }
    if (!isset($this->relative_path_cache)) {
      $this->_build_relative_caches()->save();
    }
    return $this->relative_path_cache;
  }
  /**
   * Return the relative url to this item's file.
   * @return string
   */
  public function relative_url() {
    if (!$this->loaded()) {
      return;
    }
    if (!isset($this->relative_url_cache)) {
      $this->_build_relative_caches()->save();
    }
    return $this->relative_url_cache;
  }
  /**
   * @see ORM::__get()
   */
  public function __get($column) {
    if ($column == "owner") {
      // This relationship depends on an outside module, which may not be present so handle
      // failures gracefully.
      try {
        return identity::lookup_user($this->owner_id);
      } catch (Exception $e) {
        return null;
      }
    } else {
      return parent::__get($column);
    }
  }
  /**
   * Handle any business logic necessary to create or modify an item.
   * @see ORM::save()
   *
   * @return ORM Item_Model
   */
  public function save() {
    $significant_changes = $this->changed;
    unset($significant_changes["view_count"]);
    unset($significant_changes["relative_url_cache"]);
    unset($significant_changes["relative_path_cache"]);
    if ((!empty($this->changed) && $significant_changes) || isset($this->data_file)) {
      $this->updated = time();
      if (!$this->loaded()) {
        // Create a new item.
        module::event("item_before_create", $this);
        // Set a weight if it's missing.  We don't do this in the constructor because it's not a
        // simple assignment.
        if (empty($this->weight)) {
          $this->weight = item::get_max_weight();
        }
        // Make an url friendly slug from the name, if necessary
        if (empty($this->slug)) {
          $this->slug = item::convert_filename_to_slug($this->name);
          // If the filename is all invalid characters, then the slug may be empty here.  Pick a
          // random value.
          if (empty($this->slug)) {
            $this->slug = (string)rand(1000, 9999);
          }
        }
        // Get the width, height and mime type from our data file for photos and movies.
        if ($this->is_photo() || $this->is_movie()) {
          if ($this->is_photo()) {
            list ($this->width, $this->height, $this->mime_type, $extension) =
              photo::get_file_metadata($this->data_file);
          } else if ($this->is_movie()) {
            list ($this->width, $this->height, $this->mime_type, $extension) =
              movie::get_file_metadata($this->data_file);
          }
          // Force an extension onto the name if necessary
          $pi = pathinfo($this->data_file);
          if (empty($pi["extension"])) {
            $this->name = "{$this->name}.$extension";
          }
        }
        $this->_randomize_name_or_slug_on_conflict();
        parent::save();
        // Build our url caches, then save again.  We have to do this after it's already been
        // saved once because we use only information from the database to build the paths.  If we
        // could depend on a save happening later we could defer this 2nd save.
        $this->_build_relative_caches();
        parent::save();
        // Take any actions that we can only do once all our paths are set correctly after saving.
        switch ($this->type) {
        case "album":
          mkdir($this->file_path());
          mkdir(dirname($this->thumb_path()));
          mkdir(dirname($this->resize_path()));
          break;
        case "photo":
        case "movie":
          // The thumb or resize may already exist in the case where a movie and a photo generate
          // a thumbnail of the same name (eg, foo.flv movie and foo.jpg photo will generate
          // foo.jpg thumbnail).  If that happens, randomize and save again.
          if (file_exists($this->resize_path()) ||
              file_exists($this->thumb_path())) {
            $pi = pathinfo($this->name);
            $this->name = $pi["filename"] . "-" . random::int() . "." . $pi["extension"];
            parent::save();
          }
          copy($this->data_file, $this->file_path());
          break;
        }
        // This will almost definitely trigger another save, so put it at the end so that we're
        // tail recursive.  Null out the data file variable first, otherwise the next save will
        // trigger an item_updated_data_file event.
        $this->data_file = null;
        module::event("item_created", $this);
      } else {
        // Update an existing item
        module::event("item_before_update", $item);
        // If any significant fields have changed, load up a copy of the original item and
        // keep it around.
        $original = ORM::factory("item", $this->id);
        if (array_intersect($this->changed, array("parent_id", "name", "slug"))) {
          $original->_build_relative_caches();
          $this->relative_path_cache = null;
          $this->relative_url_cache = null;
        }
        $this->_randomize_name_or_slug_on_conflict();
        parent::save();
        // Now update the filesystem and any database caches if there were significant value
        // changes.  If anything past this point fails, then we'll have an inconsistent database
        // so this code should be as robust as we can make it.
        // Update the MPTT pointers, if necessary.  We have to do this before we generate any
        // cached paths!
        if ($original->parent_id != $this->parent_id) {
          parent::move_to($this->parent());
        }
        if ($original->parent_id != $this->parent_id || $original->name != $this->name) {
          // Move all of the items associated data files
          @rename($original->file_path(), $this->file_path());
          if ($this->is_album()) {
            @rename(dirname($original->resize_path()), dirname($this->resize_path()));
            @rename(dirname($original->thumb_path()), dirname($this->thumb_path()));
          } else {
            @rename($original->resize_path(), $this->resize_path());
            @rename($original->thumb_path(), $this->thumb_path());
          }
          if ($original->parent_id != $this->parent_id) {
            // This will result in 2 events since we'll still fire the item_updated event below
            module::event("item_moved", $this, $original->parent());
          }
        }
        // Changing the name, slug or parent ripples downwards
        if ($this->is_album() &&
            ($original->name != $this->name ||
             $original->slug != $this->slug ||
             $original->parent_id != $this->parent_id)) {
          db::build()
            ->update("items")
            ->set("relative_url_cache", null)
            ->set("relative_path_cache", null)
            ->where("left_ptr", ">", $this->left_ptr)
            ->where("right_ptr", "<", $this->right_ptr)
            ->execute();
        }
        // Replace the data file, if requested.
        // @todo: we don't handle the case where you swap in a file of a different mime type
        //        should we prevent that in validation?  or in set_data_file()
        if ($this->data_file && ($this->is_photo() || $this->is_movie())) {
          copy($this->data_file, $this->file_path());
          // Get the width, height and mime type from our data file for photos and movies.
          if ($this->is_photo()) {
            list ($this->width, $this->height) = photo::get_file_metadata($this->file_path());
          } else if ($this->is_movie()) {
            list ($this->width, $this->height) = movie::get_file_metadata($this->file_path());
          }
          $this->thumb_dirty = 1;
          $this->resize_dirty = 1;
        }
        module::event("item_updated", $original, $this);
        if ($this->data_file) {
          // Null out the data file variable here, otherwise this event will trigger another
          // save() which will think that we're doing another file move.
          $this->data_file = null;
          module::event("item_updated_data_file", $this);
        }
      }
    } else if (!empty($this->changed)) {
      // Insignificant changes only.  Don't fire events or do any special checking to try to keep
      // this lightweight.
      parent::save();
    }
    return $this;
  }
  /**
   * Check to see if there's another item that occupies the same name or slug that this item
   * intends to use, and if so choose a new name/slug while preserving the extension.
   * @todo Improve this.  Random numbers are not user friendly
   */
  private function _randomize_name_or_slug_on_conflict() {
    $base_name = pathinfo($this->name, PATHINFO_FILENAME);
    $base_ext = pathinfo($this->name, PATHINFO_EXTENSION);
    $base_slug = $this->slug;
    while (ORM::factory("item")
           ->where("parent_id", "=", $this->parent_id)
           ->where("id", $this->id ? "<>" : "IS NOT", $this->id)
           ->and_open()
           ->where("name", "=", $this->name)
           ->or_where("slug", "=", $this->slug)
           ->close()
           ->find()->id) {
      $rand = random::int();
      if ($base_ext) {
        $this->name = "$base_name-$rand.$base_ext";
      } else {
        $this->name = "$base_name-$rand";
      }
      $this->slug = "$base_slug-$rand";
    }
  }
  /**
   * Return the Item_Model representing the cover for this album.
   * @return Item_Model or null if there's no cover
   */
  public function album_cover() {
    if (!$this->is_album()) {
      return null;
    }
    if (empty($this->album_cover_item_id)) {
      return null;
    }
    try {
      return model_cache::get("item", $this->album_cover_item_id);
    } catch (Exception $e) {
      // It's possible (unlikely) that the item was deleted, if so keep going.
      return null;
    }
  }
  /**
   * Find the position of the given child id in this album.  The resulting value is 1-indexed, so
   * the first child in the album is at position 1.
   *
   * This method stands as a backward compatibility for gallery 3.0, and will
   * be deprecated in version 3.1.
   */
  public function get_position($child, $where=array()) {
    return item::get_position($child, $where);
  }
  /**
   * Return an ![]() tag for the thumbnail.
   * @param array $extra_attrs  Extra attributes to add to the img tag
   * @param int (optional) $max Maximum size of the thumbnail (default: null)
   * @param boolean (optional) $center_vertically Center vertically (default: false)
   * @return string
   */
  public function thumb_img($extra_attrs=array(), $max=null, $center_vertically=false) {
    list ($height, $width) = $this->scale_dimensions($max);
    if ($center_vertically && $max) {
      // The constant is divide by 2 to calculate the file and 10 to convert to em
      $margin_top = (int)(($max - $height) / 20);
      $extra_attrs["style"] = "margin-top: {$margin_top}em";
      $extra_attrs["title"] = $this->title;
    }
    $attrs = array_merge($extra_attrs,
            array(
              "src" => $this->thumb_url(),
              "alt" => $this->title,
              "width" => $width,
              "height" => $height)
            );
    // html::image forces an absolute url which we don't want
    return "
 tag for the thumbnail.
   * @param array $extra_attrs  Extra attributes to add to the img tag
   * @param int (optional) $max Maximum size of the thumbnail (default: null)
   * @param boolean (optional) $center_vertically Center vertically (default: false)
   * @return string
   */
  public function thumb_img($extra_attrs=array(), $max=null, $center_vertically=false) {
    list ($height, $width) = $this->scale_dimensions($max);
    if ($center_vertically && $max) {
      // The constant is divide by 2 to calculate the file and 10 to convert to em
      $margin_top = (int)(($max - $height) / 20);
      $extra_attrs["style"] = "margin-top: {$margin_top}em";
      $extra_attrs["title"] = $this->title;
    }
    $attrs = array_merge($extra_attrs,
            array(
              "src" => $this->thumb_url(),
              "alt" => $this->title,
              "width" => $width,
              "height" => $height)
            );
    // html::image forces an absolute url which we don't want
    return "![]() ";
  }
  /**
   * Calculate the largest width/height that fits inside the given maximum, while preserving the
   * aspect ratio.  Don't upscale.
   * @param int $max Maximum size of the largest dimension
   * @return array
   */
  public function scale_dimensions($max) {
    $width = $this->thumb_width;
    $height = $this->thumb_height;
    if ($width <= $max && $height <= $max) {
        return array($height, $width);
    }
    if ($height) {
      if (isset($max)) {
        if ($width > $height) {
          $height = (int)($max * $height / $width);
          $width = $max;
        } else {
          $width = (int)($max * $width / $height);
          $height = $max;
        }
      }
    } else {
      // Missing thumbnail, can happen on albums with no photos yet.
      // @todo we should enforce a placeholder for those albums.
      $width = 0;
      $height = 0;
    }
    return array($height, $width);
  }
  /**
   * Return an
";
  }
  /**
   * Calculate the largest width/height that fits inside the given maximum, while preserving the
   * aspect ratio.  Don't upscale.
   * @param int $max Maximum size of the largest dimension
   * @return array
   */
  public function scale_dimensions($max) {
    $width = $this->thumb_width;
    $height = $this->thumb_height;
    if ($width <= $max && $height <= $max) {
        return array($height, $width);
    }
    if ($height) {
      if (isset($max)) {
        if ($width > $height) {
          $height = (int)($max * $height / $width);
          $width = $max;
        } else {
          $width = (int)($max * $width / $height);
          $height = $max;
        }
      }
    } else {
      // Missing thumbnail, can happen on albums with no photos yet.
      // @todo we should enforce a placeholder for those albums.
      $width = 0;
      $height = 0;
    }
    return array($height, $width);
  }
  /**
   * Return an ![]() tag for the resize.
   * @param array $extra_attrs  Extra attributes to add to the img tag
   * @return string
   */
  public function resize_img($extra_attrs) {
    $attrs = array_merge($extra_attrs,
            array("src" => $this->resize_url(),
                  "alt" => $this->title,
                  "width" => $this->resize_width,
                  "height" => $this->resize_height)
            );
    // html::image forces an absolute url which we don't want
    return "
 tag for the resize.
   * @param array $extra_attrs  Extra attributes to add to the img tag
   * @return string
   */
  public function resize_img($extra_attrs) {
    $attrs = array_merge($extra_attrs,
            array("src" => $this->resize_url(),
                  "alt" => $this->title,
                  "width" => $this->resize_width,
                  "height" => $this->resize_height)
            );
    // html::image forces an absolute url which we don't want
    return "![]() ";
  }
  /**
   * Return a flowplayer
";
  }
  /**
   * Return a flowplayer