diff options
Diffstat (limited to 'modules/gallery/models/item.php')
| -rw-r--r-- | modules/gallery/models/item.php | 513 | 
1 files changed, 382 insertions, 131 deletions
| diff --git a/modules/gallery/models/item.php b/modules/gallery/models/item.php index 6851e1a3..e4ff0bfb 100644 --- a/modules/gallery/models/item.php +++ b/modules/gallery/models/item.php @@ -18,15 +18,24 @@   * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.   */  class Item_Model extends ORM_MPTT { -  protected $children = 'items'; +  protected $children = "items";    protected $sorting = array(); +  protected $data_file = null; -  var $form_rules = array( -    "name" => "required|length[0,255]", -    "title" => "required|length[0,255]", -    "description" => "length[0,65535]", -    "slug" => "required|length[0,255]" -  ); +  public function __construct($id=null) { +    parent::__construct($id); + +    if (!$this->loaded()) { +      // Set reasonable defaults +      $this->created = time(); +      $this->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); +      $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 @@ -98,98 +107,12 @@ class Item_Model extends ORM_MPTT {    }    /** -   * Move this item to the specified target. -   * @chainable -   * @param   Item_Model $target  Target item (must be an album) -   * @return  ORM_MPTT -   */ -  function move_to($target) { -    if (!$target->is_album()) { -      throw new Exception("@todo INVALID_MOVE_TYPE $target->type"); -    } - -    if (file_exists($target_file = "{$target->file_path()}/$this->name")) { -      throw new Exception("@todo INVALID_MOVE_TARGET_EXISTS: $target_file"); -    } - -    if ($this->id == 1) { -      throw new Exception("@todo INVALID_SOURCE root album"); -    } - -    $original_path = $this->file_path(); -    $original_resize_path = $this->resize_path(); -    $original_thumb_path = $this->thumb_path(); -    $original_parent = $this->parent(); - -    parent::move_to($target, true); -    model_cache::clear(); -    $this->relative_path_cache = null; - -    rename($original_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())); -      db::build() -        ->update("items") -        ->set("relative_path_cache", null) -        ->set("relative_url_cache", null) -        ->where("left_ptr", ">", $this->left_ptr) -        ->where("right_ptr", "<", $this->right_ptr) -        ->execute(); -    } else { -      @rename($original_resize_path, $this->resize_path()); -      @rename($original_thumb_path, $this->thumb_path()); -    } - -    module::event("item_moved", $this, $original_parent); -    return $this; -  } - -  /** -   * Rename the underlying file for this item to a new name.  Move all the files.  This requires a -   * save. -   * +   * Specify the path to the data file associated with this item.  To actually associate it, +   * you still have to call save().     * @chainable     */ -  public function rename($new_name) { -    if ($new_name == $this->name) { -      return; -    } - -    if (strpos($new_name, "/")) { -      throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH"); -    } - -    $old_relative_path = urldecode($this->relative_path()); -    $new_relative_path = dirname($old_relative_path) . "/" . $new_name; -    if (file_exists(VARPATH . "albums/$new_relative_path")) { -      throw new Exception("@todo INVALID_RENAME_FILE_EXISTS: $new_relative_path"); -    } - -    @rename(VARPATH . "albums/$old_relative_path", VARPATH . "albums/$new_relative_path"); -    @rename(VARPATH . "resizes/$old_relative_path", VARPATH . "resizes/$new_relative_path"); -    if ($this->is_movie()) { -      // Movie thumbnails have a .jpg extension -      $old_relative_thumb_path = preg_replace("/...$/", "jpg", $old_relative_path); -      $new_relative_thumb_path = preg_replace("/...$/", "jpg", $new_relative_path); -      @rename(VARPATH . "thumbs/$old_relative_thumb_path", -              VARPATH . "thumbs/$new_relative_thumb_path"); -    } else { -      @rename(VARPATH . "thumbs/$old_relative_path", VARPATH . "thumbs/$new_relative_path"); -    } - -    $this->name = $new_name; - -    if ($this->is_album()) { -      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(); -    } - +  public function set_data_file($data_file) { +    $this->data_file = $data_file;      return $this;    } @@ -323,7 +246,7 @@ class Item_Model extends ORM_MPTT {      }      $this->relative_path_cache = implode($names, "/");      $this->relative_url_cache = implode($slugs, "/"); -    $this->save(); +    return $this;    }    /** @@ -338,7 +261,7 @@ class Item_Model extends ORM_MPTT {      }      if (!isset($this->relative_path_cache)) { -      $this->_build_relative_caches(); +      $this->_build_relative_caches()->save();      }      return $this->relative_path_cache;    } @@ -353,7 +276,7 @@ class Item_Model extends ORM_MPTT {      }      if (!isset($this->relative_url_cache)) { -      $this->_build_relative_caches(); +      $this->_build_relative_caches()->save();      }      return $this->relative_url_cache;    } @@ -376,30 +299,10 @@ class Item_Model extends ORM_MPTT {    }    /** -   * @see ORM::__set() -   */ -  public function __set($column, $value) { -    if ($column == "name") { -      $this->relative_path_cache = null; -    } else if ($column == "slug") { -      if ($this->slug != $value) { -        // Clear the relative url cache for this item and all children -        $this->relative_url_cache = null; -        if ($this->is_album()) { -          db::build() -            ->update("items") -            ->set("relative_url_cache", null) -            ->where("left_ptr", ">", $this->left_ptr) -            ->where("right_ptr", "<", $this->right_ptr) -            ->execute(); -        } -      } -    } -    parent::__set($column, $value); -  } - -  /** +   * 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; @@ -410,18 +313,168 @@ class Item_Model extends ORM_MPTT {      if (!empty($this->changed) && $significant_changes) {        $this->updated = time();        if (!$this->loaded()) { -        $this->created = $this->updated; -        $this->weight = item::get_max_weight(); +        // Create a new item. + +        // 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)) { +          $tmp = pathinfo($this->name, PATHINFO_FILENAME); +          $tmp = preg_replace("/[^A-Za-z0-9-_]+/", "-", $tmp); +          $this->slug = trim($tmp, "-"); +        } + +        // Get the width, height and mime type from our data file for photos and movies. +        if ($this->is_movie() || $this->is_photo()) { +          $pi = pathinfo($this->data_file); + +          if ($this->is_photo()) { +            $image_info = getimagesize($this->data_file); +            $this->width = $image_info[0]; +            $this->height = $image_info[1]; +            $this->mime_type = $image_info["mime"]; + +            // Force an extension onto the name if necessary +            if (empty($pi["extension"])) { +              $pi["extension"] = image_type_to_extension($image_info[2], false); +              $this->name .= "." . $pi["extension"]; +            } +          } else { +            list ($this->width, $this->height) = movie::getmoviesize($this->data_file); + +            // No extension?  Assume FLV. +            if (empty($pi["extension"])) { +              $pi["extension"] = "flv"; +              $this->name .= "." . $pi["extension"]; +            } + +            $this->mime_type = strtolower($pi["extension"]) == "mp4" ? "video/mp4" : "video/x-flv"; +          } +        } + +        // Randomize the name or slug if there's a conflict.  Preserve the extension. +        // @todo Improve this.  Random numbers are not user friendly +        $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) +               ->and_open() +               ->where("name", "=", $this->name) +               ->or_where("slug", "=", $this->slug) +               ->close() +               ->find()->id) { +          $rand = rand(); +          if ($base_ext) { +            $this->name = "$base_name-$rand.$base_ext"; +          } else { +            $this->name = "$base_name-$rand"; +          } +          $this->slug = "$base_slug-$rand"; +        } + +        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"] . "-" . rand() . "." . $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. +        module::event("item_created", $this);        } else { -        $send_event = 1; +        // Update an existing item + +        // If any significant fields have changed, load up a copy of the original item and +        // keep it around. +        $original = ORM::factory("item")->where("id", "=", $this->id)->find(); +        if (array_intersect($this->changed, array("parent_id", "name", "slug"))) { +          $original->_build_relative_caches(); +          $this->relative_path_cache = null; +          $this->relative_url_cache = null; +        } + +        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(); +        } + +        module::event("item_updated", $original, $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();      } -    $original = clone $this->original(); -    parent::save(); -    if (isset($send_event)) { -      module::event("item_updated", $original, $this); -    }      return $this;    } @@ -657,4 +710,202 @@ class Item_Model extends ORM_MPTT {      }      return parent::descendants($limit, $offset, $where, $order_by);    } + +  /** +   * Specify our rules here so that we have access to the instance of this model. +   */ +  public function validate($array=null) { +    if (!$array) { +      $this->rules = array( +        "album_cover_item_id" => array("callbacks" => array(array($this, "valid_album_cover"))), +        "description"         => array("rules"     => array("length[0,65535]")), +        "mime_type"           => array("callbacks" => array(array($this, "valid_field"))), +        "name"                => array("rules"     => array("length[0,255]", "required"), +                                       "callbacks" => array(array($this, "valid_name"))), +        "parent_id"           => array("callbacks" => array(array($this, "valid_parent"))), +        "rand_key"            => array("rule"      => array("decimal")), +        "slug"                => array("rules"     => array("length[0,255]", "required"), +                                       "callbacks" => array(array($this, "valid_slug"))), +        "sort_column"         => array("callbacks" => array(array($this, "valid_field"))), +        "sort_order"          => array("callbacks" => array(array($this, "valid_field"))), +        "title"               => array("rules"     => array("length[0,255]", "required")), +        "type"                => array("callbacks" => array(array($this, "read_only"), +                                                            array($this, "valid_field"))), +      ); + +      // Conditional rules +      if ($this->id == 1) { +        // Root album can't have a name or slug so replace the rules +        $this->rules["name"] = array("rules" => array("length[0]")); +        $this->rules["slug"] = array("rules" => array("length[0]")); +      } + +      // Movies and photos must have data files +      if (($this->is_photo() || $this->is_movie()) && !$this->loaded()) { +        $this->rules["name"]["callbacks"][] = array($this, "valid_data_file"); +      } +    } + +    parent::validate($array); +  } + +  /** +   * Validate that the desired slug does not conflict. +   */ +  public function valid_slug(Validation $v, $field) { +    if (preg_match("/[^A-Za-z0-9-_]/", $this->slug)) { +      $v->add_error("slug", "not_url_safe"); +    } else if (db::build() +        ->from("items") +        ->where("parent_id", "=", $this->parent_id) +        ->where("id", "<>", $this->id) +        ->where("slug", "=", $this->slug) +        ->count_records()) { +      $v->add_error("slug", "conflict"); +    } +  } + +  /** +   * Validate the item name.  It can't conflict with other names, can't contain slashes or +   * trailing periods. +   */ +  public function valid_name(Validation $v, $field) { +    if (strpos($this->name, "/") !== false) { +      $v->add_error("name", "no_slashes"); +      return; +    } else if (rtrim($this->name, ".") !== $this->name) { +      $v->add_error("name", "no_trailing_period"); +      return; +    } + +    if ($this->is_movie() || $this->is_photo()) { +      if ($this->loaded()) { +        // Existing items can't change their extension +        $original = ORM::factory("item")->where("id", "=", $this->id)->find(); +        $new_ext = pathinfo($this->name, PATHINFO_EXTENSION); +        $old_ext = pathinfo($original->name, PATHINFO_EXTENSION); +        if (strcasecmp($new_ext, $old_ext)) { +          $v->add_error("name", "illegal_data_file_extension"); +          return; +        } +      } else { +        // New items must have an extension +        if (!pathinfo($this->name, PATHINFO_EXTENSION)) { +          $v->add_error("name", "illegal_data_file_extension"); +          return; +        } +      } +    } + +    if (db::build() +        ->from("items") +        ->where("parent_id", "=", $this->parent_id) +        ->where("name", "=", $this->name) +        ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null) +        ->count_records()) { +      $v->add_error("name", "conflict"); +      return; +    } +  } + +  /** +   * Make sure that the data file is well formed (it exists and isn't empty). +   */ +  public function valid_data_file(Validation $v, $field) { +    if (!is_file($this->data_file)) { +      $v->add_error("name", "bad_data_file_path"); +    } else if (filesize($this->data_file) == 0) { +      $v->add_error("name", "empty_data_file"); +    } +  } + +  /** +   * Make sure that the parent id refers to an album. +   */ +  public function valid_parent(Validation $v, $field) { +    if ($this->id == 1) { +      if ($this->parent_id != 0) { +        $v->add_error("parent_id", "invalid"); +      } +    } else { +      $query = db::build() +        ->from("items") +        ->where("id", "=", $this->parent_id) +        ->where("type", "=", "album"); + +      // If this is an existing item, make sure the new parent is not part of our hierarchy +      if ($this->loaded()) { +        $query->and_open() +          ->where("left_ptr", "<", $this->left_ptr) +          ->or_where("right_ptr", ">", $this->right_ptr) +          ->close(); +      } + +      if ($query->count_records() != 1) { +        $v->add_error("parent_id", "invalid"); +      } +    } +  } + +  /** +   * Make sure the album cover item id refers to a valid item, or is null. +   */ +  public function valid_album_cover(Validation $v, $field) { +    if ($this->id == 1) { +      return; +    } + +    if ($this->album_cover_item_id && db::build() +        ->from("items") +        ->where("id", "=", $this->album_cover_item_id) +        ->count_records() != 1) { +      $v->add_error("album_cover_item_id", "invalid_item"); +    } +  } + +  /** +   * Make sure that the type is valid. +   */ +  public function valid_field(Validation $v, $field) { +    switch($field) { +    case "mime_type": +      if ($this->is_movie()) { +        $legal_values = array("video/flv", "video/x-flv", "video/mp4"); +      } if ($this->is_photo()) { +        $legal_values = array("image/jpeg", "image/gif", "image/png"); +      } +      break; + +    case "sort_column": +      if (!array_key_exists($this->sort_column, $this->object)) { +        $v->add_error($field, "invalid"); +      } +      break; + +    case "sort_order": +      $legal_values = array("ASC", "DESC", "asc", "desc"); +      break; + +    case "type": +      $legal_values = array("album", "photo", "movie"); +      break; + +    default: +      $v->add_error($field, "unvalidated_field"); +      break; +    } + +    if (isset($legal_values) && !in_array($this->$field, $legal_values)) { +      $v->add_error($field, "invalid"); +    } +  } + +  /** +   * This field cannot be changed after it's been set. +   */ +  public function read_only(Validation $v, $field) { +    if ($this->loaded() && isset($this->changed[$field])) { +      $v->add_error($field, "read_only"); +    } +  }  } | 
