summaryrefslogtreecommitdiff
path: root/modules/gallery/models/item.php
diff options
context:
space:
mode:
Diffstat (limited to 'modules/gallery/models/item.php')
-rw-r--r--modules/gallery/models/item.php402
1 files changed, 339 insertions, 63 deletions
diff --git a/modules/gallery/models/item.php b/modules/gallery/models/item.php
index 6851e1a3..fcd9b8e6 100644
--- a/modules/gallery/models/item.php
+++ b/modules/gallery/models/item.php
@@ -18,15 +18,9 @@
* 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();
-
- var $form_rules = array(
- "name" => "required|length[0,255]",
- "title" => "required|length[0,255]",
- "description" => "length[0,65535]",
- "slug" => "required|length[0,255]"
- );
+ protected $data_file = null;
/**
* Add a set of restrictions to any following queries to restrict access only to items
@@ -146,21 +140,12 @@ class Item_Model extends ORM_MPTT {
}
/**
- * Rename the underlying file for this item to a new name. Move all the files. This requires a
- * save.
+ * Rename the underlying file for this item to a new name and move all related files.
*
* @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());
+ private function rename($new_name) {
+ $old_relative_path = urldecode($this->original()->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");
@@ -178,18 +163,16 @@ class Item_Model extends ORM_MPTT {
@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();
- }
+ return $this;
+ }
+ /**
+ * 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;
}
@@ -323,7 +306,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 +321,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 +336,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 +359,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 +373,147 @@ class Item_Model extends ORM_MPTT {
if (!empty($this->changed) && $significant_changes) {
$this->updated = time();
if (!$this->loaded()) {
+ // Create a new item. Use whatever fields are set, and specify defaults for the rest.
$this->created = $this->updated;
$this->weight = item::get_max_weight();
+ $this->rand_key = ((float)mt_rand()) / (float)mt_getrandmax();
+ $this->thumb_dirty = 1;
+ $this->resize_dirty = 1;
+ if (empty($this->sort_column)) {
+ $this->sort_column = "created";
+ }
+ if (empty($this->sort_order)) {
+ $this->sort_order = "ASC";
+ }
+ if (empty($this->owner_id)) {
+ $this->owner_id = identity::active_user()->id;
+ }
+
+ // 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 ($this->original()->name != $this->name) {
+ $this->rename($this->name);
+ $this->relative_path_cache = null;
+ }
+
+ if ($this->original()->slug != $this->slug) {
+ // Clear the relative url cache for this item and all children
+ $this->relative_url_cache = null;
+ }
+
+ // Changing the name or the slug ripples downwards
+ if ($this->is_album() &&
+ ($this->original()->name != $this->name ||
+ $this->original()->slug != $this->slug)) {
+ 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();
+ }
+ $original = clone $this->original();
+ parent::save();
+ 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 +749,188 @@ 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_item"))),
+ "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;
+ }
+
+ 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
+ $new_ext = pathinfo($this->name, PATHINFO_EXTENSION);
+ $old_ext = pathinfo($this->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");
+ }
+ }
+ }
+
+ if (db::build()
+ ->from("items")
+ ->where("parent_id", "=", $this->parent_id)
+ ->where("id", "<>", $this->id)
+ ->where("name", "=", $this->name)
+ ->count_records()) {
+ $v->add_error("name", "conflict");
+ }
+ }
+
+ /**
+ * 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 {
+ if (db::build()
+ ->from("items")
+ ->where("id", "=", $this->parent_id)
+ ->where("type", "=", "album")
+ ->count_records() != 1) {
+ $v->add_error("parent_id", "invalid");
+ }
+ }
+ }
+
+ /**
+ * Make sure the field refers to a valid item by id, or is null.
+ */
+ public function valid_item(Validation $v, $field) {
+ if ($this->$field && db::build()
+ ->from("items")
+ ->where("id", "=", $this->$field)
+ ->count_records() != 1) {
+ $v->add_error($field, "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/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() && $this->original()->$field != $this->$field) {
+ $v->add_error($field, "read_only");
+ }
+ }
}