summaryrefslogtreecommitdiff
path: root/modules/gallery
diff options
context:
space:
mode:
Diffstat (limited to 'modules/gallery')
-rw-r--r--modules/gallery/controllers/albums.php93
-rw-r--r--modules/gallery/helpers/album.php77
-rw-r--r--modules/gallery/helpers/gallery_rest.php299
-rw-r--r--modules/gallery/helpers/item.php34
-rw-r--r--modules/gallery/helpers/movie.php1
-rw-r--r--modules/gallery/helpers/photo.php1
-rw-r--r--modules/gallery/libraries/ORM_MPTT.php82
-rw-r--r--modules/gallery/models/item.php214
-rw-r--r--modules/gallery/tests/Gallery_Rest_Helper_Test.php3
9 files changed, 356 insertions, 448 deletions
diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php
index 2eeefdf1..7658a913 100644
--- a/modules/gallery/controllers/albums.php
+++ b/modules/gallery/controllers/albums.php
@@ -95,30 +95,36 @@ class Albums_Controller extends Items_Controller {
access::required("view", $album);
access::required("add", $album);
- $input = Input::instance();
$form = album::get_add_form($album);
- if ($form->validate()) {
- $new_album = album::create(
- $album,
- $input->post("name"),
- $input->post("title", $input->post("name")),
- $input->post("description"),
- identity::active_user()->id,
- $input->post("slug"));
+ try {
+ $valid = $form->validate();
+ $album = ORM::factory("item");
+ $album->type = "album";
+ $album->parent_id = $parent_id;
+ $album->name = $form->add_album->inputs["name"]->value;
+ $album->title = $form->add_album->title->value ?
+ $form->add_album->title->value : $form->add_album->inputs["name"]->value;
+ $album->description = $form->add_album->description->value;
+ $album->slug = $form->add_album->slug->value;
+ $album->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->add_album->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+ if ($valid) {
+ $album->save();
log::success("content", "Created an album",
- html::anchor("albums/$new_album->id", "view album"));
+ html::anchor("albums/$album->id", "view album"));
message::success(t("Created album %album_title",
- array("album_title" => html::purify($new_album->title))));
+ array("album_title" => html::purify($album->title))));
- print json_encode(
- array("result" => "success",
- "location" => $new_album->url()));
+ print json_encode(array("result" => "success", "location" => $album->url()));
} else {
- print json_encode(
- array(
- "result" => "error",
- "form" => $form->__toString()));
+ print json_encode(array("result" => "error", "form" => (string) $form));
}
}
@@ -129,42 +135,27 @@ class Albums_Controller extends Items_Controller {
access::required("edit", $album);
$form = album::get_edit_form($album);
- if ($valid = $form->validate()) {
- if ($album->id != 1 &&
- $form->edit_item->dirname->value != $album->name ||
- $form->edit_item->slug->value != $album->slug) {
- // Make sure that there's not a conflict
- if ($row = db::build()
- ->select(array("name", "slug"))
- ->from("items")
- ->where("parent_id", "=", $album->parent_id)
- ->where("id", "<>", $album->id)
- ->and_open()
- ->where("name", "=", $form->edit_item->dirname->value)
- ->or_where("slug", "=", $form->edit_item->slug->value)
- ->close()
- ->execute()
- ->current()) {
- if ($row->name == $form->edit_item->dirname->value) {
- $form->edit_item->dirname->add_error("name_conflict", 1);
- }
- if ($row->slug == $form->edit_item->slug->value) {
- $form->edit_item->slug->add_error("slug_conflict", 1);
- }
- $valid = false;
- }
- }
- }
-
- if ($valid) {
+ try {
+ $valid = $form->validate();
$album->title = $form->edit_item->title->value;
$album->description = $form->edit_item->description->value;
$album->sort_column = $form->edit_item->sort_order->column->value;
$album->sort_order = $form->edit_item->sort_order->direction->value;
- if ($album->id != 1) {
- $album->rename($form->edit_item->dirname->value);
- }
+ $album->name = $form->edit_item->dirname->value;
$album->slug = $form->edit_item->slug->value;
+ $album->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ if ($key == "name") {
+ $key = "dirname";
+ }
+ $form->edit_item->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
$album->save();
module::event("item_edit_form_completed", $album, $form);
@@ -180,9 +171,7 @@ class Albums_Controller extends Items_Controller {
print json_encode(array("result" => "success"));
}
} else {
- print json_encode(
- array("result" => "error",
- "form" => $form->__toString()));
+ print json_encode(array("result" => "error", "form" => (string) $form));
}
}
diff --git a/modules/gallery/helpers/album.php b/modules/gallery/helpers/album.php
index feaf74cc..e99770e9 100644
--- a/modules/gallery/helpers/album.php
+++ b/modules/gallery/helpers/album.php
@@ -24,72 +24,6 @@
* Note: by design, this class does not do any permission checking.
*/
class album_Core {
- /**
- * Create a new album.
- * @param integer $parent_id id of parent album
- * @param string $name the name of this new album (it will become the directory name on disk)
- * @param integer $title the title of the new album
- * @param string $description (optional) the longer description of this album
- * @param string $slug (optional) the url component for this photo
- * @return Item_Model
- */
- static function create($parent, $name, $title, $description=null, $owner_id=null, $slug=null) {
- if (!$parent->loaded() || !$parent->is_album()) {
- throw new Exception("@todo INVALID_PARENT");
- }
-
- if (strpos($name, "/")) {
- throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH");
- }
-
- // We don't allow trailing periods as a security measure
- // ref: http://dev.kohanaphp.com/issues/684
- if (rtrim($name, ".") != $name) {
- throw new Exception("@todo NAME_CANNOT_END_IN_PERIOD");
- }
-
- if (empty($slug)) {
- $slug = item::convert_filename_to_slug($name);
- }
-
- $album = ORM::factory("item");
- $album->type = "album";
- $album->title = $title;
- $album->description = $description;
- $album->name = $name;
- $album->owner_id = $owner_id;
- $album->thumb_dirty = 1;
- $album->resize_dirty = 1;
- $album->slug = $slug;
- $album->rand_key = ((float)mt_rand()) / (float)mt_getrandmax();
- $album->sort_column = "created";
- $album->sort_order = "ASC";
-
- // Randomize the name or slug if there's a conflict
- // @todo Improve this. Random numbers are not user friendly
- while (ORM::factory("item")
- ->where("parent_id", "=", $parent->id)
- ->and_open()
- ->where("name", "=", $album->name)
- ->or_where("slug", "=", $album->slug)
- ->close()
- ->find()->id) {
- $rand = rand();
- $album->name = "{$name}-$rand";
- $album->slug = "{$slug}-$rand";
- }
-
- $album = $album->add_to_parent($parent);
- mkdir($album->file_path());
- mkdir(dirname($album->thumb_path()));
- mkdir(dirname($album->resize_path()));
-
- // @todo: publish this from inside Item_Model::save() when we refactor to the point where
- // there's only one save() happening here.
- module::event("item_created", $album);
-
- return $album;
- }
static function get_add_form($parent) {
$form = new Forge("albums/create/{$parent->id}", "", "post", array("id" => "g-add-album-form"));
@@ -98,16 +32,13 @@ class album_Core {
$group->input("title")->label(t("Title"));
$group->textarea("description")->label(t("Description"));
$group->input("name")->label(t("Directory name"))
- ->callback("item::validate_no_slashes")
->error_messages("no_slashes", t("The directory name can't contain the \"/\" character"));
$group->input("slug")->label(t("Internet Address"))
- ->callback("item::validate_url_safe")
->error_messages(
"not_url_safe",
t("The internet address should contain only letters, numbers, hyphens and underscores"));
$group->hidden("type")->value("album");
$group->submit("")->value(t("Create"));
- $form->add_rules_from(ORM::factory("item"));
$form->script("")
->url(url::abs_file("modules/gallery/js/albums_form_add.js"));
return $form;
@@ -124,15 +55,12 @@ class album_Core {
$group->input("dirname")->label(t("Directory Name"))->value($parent->name)
->rules("required")
->error_messages(
- "name_conflict", t("There is already a movie, photo or album with this name"))
- ->callback("item::validate_no_slashes")
+ "conflict", t("There is already a movie, photo or album with this name"))
->error_messages("no_slashes", t("The directory name can't contain a \"/\""))
- ->callback("item::validate_no_trailing_period")
->error_messages("no_trailing_period", t("The directory name can't end in \".\""));
$group->input("slug")->label(t("Internet Address"))->value($parent->slug)
->error_messages(
- "slug_conflict", t("There is already a movie, photo or album with this internet address"))
- ->callback("item::validate_url_safe")
+ "conflict", t("There is already a movie, photo or album with this internet address"))
->error_messages(
"not_url_safe",
t("The internet address should contain only letters, numbers, hyphens and underscores"));
@@ -159,7 +87,6 @@ class album_Core {
$group = $form->group("buttons")->label("");
$group->hidden("type")->value("album");
$group->submit("")->value(t("Modify"));
- $form->add_rules_from(ORM::factory("item"));
return $form;
}
diff --git a/modules/gallery/helpers/gallery_rest.php b/modules/gallery/helpers/gallery_rest.php
index a87ebb4e..0de5da2b 100644
--- a/modules/gallery/helpers/gallery_rest.php
+++ b/modules/gallery/helpers/gallery_rest.php
@@ -17,232 +17,139 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
-class gallery_rest_Core {
- static function get($request) {
- $path = implode("/", $request->arguments);
-
- $item = gallery_rest::_get_item($path);
-
- $parent = $item->parent();
- $response_data = array("type" => $item->type,
- "name" => $item->name,
- "path" => $item->relative_url(),
- "parent_path" => empty($parent) ? null : $parent->relative_url(),
- "title" => $item->title,
- "thumb_url" => $item->thumb_url(true),
- "thumb_size" => array("height" => $item->thumb_height,
- "width" => $item->thumb_width),
- "resize_url" => $item->resize_url(true),
- "resize_size" => array("height" => (int)$item->resize_height,
- "width" => (int)$item->resize_width),
- "url" => $item->file_url(true),
- "size" => array("height" => $item->height,
- "width" => $item->width),
- "description" => $item->description,
- "slug" => $item->slug);
-
- $children = self::_get_children($item, $request);
- if (!empty($children) || $item->is_album()) {
- $response_data["children"] = $children;
- }
- return rest::success(array("resource" => $response_data));
- }
- static function put($request) {
- if (empty($request->arguments)) {
- throw new Rest_Exception(400, "Bad request");
- }
- $path = implode("/", $request->arguments);
- $item = gallery_rest::_get_item($path, "edit");
-
- // Validate the request data
- $new_values = gallery_rest::_validate($request, $item->parent_id, $item->id);
- $errors = $new_values->errors();
- if (empty($errors)) {
- $item->title = $new_values->title;
- $item->description = $new_values->description;
- if ($item->id != 1) {
- $item->rename($new_values->name);
- }
- $item->slug = $new_values->slug;
- $item->save();
+// @todo Add logging
+// @todo VALIDATION
+
+// Validation questions
+//
+// We need to be able to properly validate anything we want to enter here. But all of our
+// validation currently happens at the controller / form level, and we're not using the same
+// controllers or forms.
+//
+// Possible solutions:
+// 1) Move validation into the model and use it both here and in the regular controllers. But
+// if we do that, how do we translate validation failures into a user-consumable output which
+// we need so that we can return proper error responses to form submissions?
+//
+// 2) Create some kind of validation helper that can validate every field. Wait, isn't this
+// just like #1 except in a helper instead of in the model?
- log::success("content", "Updated $item->type",
- "<a href=\"{$item->type}s/$item->id\">view</a>");
+class gallery_rest_Core {
- return rest::success();
- } else {
- return rest::validation_error($errors);
- }
- }
+ /**
+ * For items that are collections, you can specify the following additional query parameters to
+ * query the collection. You can specify them in any combination.
+ *
+ * scope=direct
+ * only return items that are immediately under this one
+ * scope=all
+ * return items anywhere under this one
+ *
+ * name=<substring>
+ * only return items where the name contains this substring
+ *
+ * random=true
+ * return a single random item
+ *
+ * type=<comma separate list of photo, movie or album>
+ * limit the type to types in this list. eg, "type=photo,movie"
+ */
+ static function get($request) {
+ $item = rest::resolve($request->url);
+ access::required("view", $item);
- static function post($request) {
- if (empty($request->arguments)) {
- throw new Rest_Exception(400, "Bad request");
+ $p = $request->params;
+ if (isset($p->random)) {
+ $orm = item::random_query()->offset(0)->limit(1);
+ } else {
+ $orm = ORM::factory("item")->viewable();
}
- $components = $request->arguments;
- $name = urldecode(array_pop($components));
-
- $parent = gallery_rest::_get_item(implode("/", $components), "edit");
-
- // Validate the request data
- $request->name = $name;
- $new_values = gallery_rest::_validate($request, $parent->id);
- $errors = $new_values->errors();
- if (!empty($errors)) {
- return rest::validation_error($errors);
+ if (!empty($p->scope) && !in_array($p->scope, array("direct", "all"))) {
+ throw new Exception("Bad Request", 400);
}
-
- if (empty($new_values["image"])) {
- $new_item = album::create(
- $parent,
- $name,
- empty($new_values["title"]) ? $name : $new_values["title"],
- empty($new_values["description"]) ? null : $new_values["description"],
- identity::active_user()->id,
- empty($new_values["slug"]) ? $name : $new_values["slug"]);
- $log_message = t("Added an album");
- } else {
- $temp_filename = upload::save("image");
- $path_info = @pathinfo($temp_filename);
- if (array_key_exists("extension", $path_info) &&
- in_array(strtolower($path_info["extension"]), array("flv", "mp4"))) {
- $new_item =
- movie::create($parent, $temp_filename, $new_values["name"], $new_values["title"]);
- $log_message = t("Added a movie");
+ if (!empty($p->scope)) {
+ if ($p->scope == "direct") {
+ $orm->where("parent_id", "=", $item->id);
} else {
- $new_item =
- photo::create($parent, $temp_filename, $new_values["name"], $new_values["title"]);
- $log_message = t("Added a photo");
+ $orm->where("left_ptr", ">=", $item->left_ptr);
+ $orm->where("right_ptr", "<=", $item->left_ptr);
+ $orm->where("id", "<>", $item->id);
}
}
- log::success("content", $log_message, "<a href=\"{$new_item->type}s/$new_item->id\">view</a>");
-
- return rest::success(array("path" => $new_item->relative_url()));
- }
-
- static function delete($request) {
- if (empty($request->arguments)) {
- throw new Rest_Exception(400, "Bad request");
+ if (isset($p->name)) {
+ $orm->where("name", "LIKE", "%{$p->name}%");
}
- $path = implode("/", $request->arguments);
-
- $item = gallery_rest::_get_item($path, "edit");
- if ($item->id == 1) {
- throw new Rest_Exception(400, "Bad request");
+ if (isset($p->type)) {
+ $orm->where("type", "IN", explode(",", $p->type));
}
- $parent = $item->parent();
- $item->delete();
-
- if ($item->is_album()) {
- $msg = t("Deleted album <b>%title</b>", array("title" => html::purify($item->title)));
- } else {
- $msg = t("Deleted photo <b>%title</b>", array("title" => html::purify($item->title)));
+ $members = array();
+ foreach ($orm->find_all() as $child) {
+ $members[] = url::abs_site("rest/gallery/" . $child->relative_url());
}
- log::success("content", $msg);
- return rest::success(array("resource" => array("parent_path" => $parent->relative_url())));
+ return rest::reply(array("resource" => $item->as_array(), "members" => $members));
}
- private static function _get_item($path, $permission="view") {
- $item = url::get_item_from_uri($path);
-
- if (!$item->loaded()) {
- throw new Kohana_404_Exception();
- }
-
- if (!access::can($permission, $item)) {
- throw new Kohana_404_Exception();
+ static function put($request) {
+ $item = rest::resolve($request->url);
+ access::required("edit", $item);
+
+ $params = $request->params;
+ foreach (array("captured", "description", "slug", "sort_column", "sort_order",
+ "title", "view_count", "weight") as $key) {
+ if (isset($params->$key)) {
+ $item->$key = $params->$key;
+ }
}
+ $item->save();
- return $item;
+ return rest::reply(array("url" => url::abs_site("/rest/gallery/" . $item->relative_url())));
}
- private static function _get_children($item, $request) {
- $children = array();
- $limit = empty($request->limit) ? null : $request->limit;
- $offset = empty($request->offset) ? null : $request->offset;
- $where = empty($request->filter) ? array() : array("type" => $request->filter);
- foreach ($item->viewable()->children($limit, $offset, $where) as $child) {
- $children[] = array("type" => $child->type,
- "has_children" => $child->children_count() > 0,
- "path" => $child->relative_url(),
- "thumb_url" => $child->thumb_url(true),
- "thumb_dimensions" => array("width" => $child->thumb_width,
- "height" => $child->thumb_height),
- "has_thumb" => $child->has_thumb(),
- "title" => $child->title);
- }
-
- return $children;
- }
+ static function post($request) {
+ $parent = rest::resolve($request->url);
+ access::required("edit", $parent);
- private static function _validate($request, $parent_id, $item_id=0) {
- $item = ORM::factory("item", $item_id);
+ $params = $request->params;
+ switch ($params->type) {
+ case "album":
+ $item = album::create(
+ $parent,
+ $params->name,
+ isset($params->title) ? $params->title : $name,
+ isset($params->description) ? $params->description : null);
+ break;
- // Normalize the inputs so all fields have a value
- $new_values = Validation::factory(array());
- foreach ($item->form_rules as $field => $rule_set) {
- if (isset($request->$field)) {
- $new_values[$field] = $request->$field;
- } else if (isset($item->$field)) {
- $new_values[$field] = $item->$field;
- }
- foreach (explode("|", $rule_set) as $rule) {
- $new_values->add_rules($field, $rule);
- }
- }
- $name = $new_values["name"];
- $new_values["title"] = empty($new_values["title"]) ? $name : $new_values["title"];
- $new_values["description"] =
- empty($new_values["description"]) ? null : $new_values["description"];
- $new_values["slug"] = empty($new_values["slug"]) ? $name : $new_values["slug"];
-
- if (!empty($request->image)) {
- $new_values["image"] = $request->image;
- $new_values->add_rules(
- "image", "upload::valid", "upload::required", "upload::type[gif,jpg,jpeg,png,flv,mp4]");
- }
+ case "photo":
+ $item = photo::create(
+ $parent,
+ $request->file,
+ $params->name,
+ isset($params->title) ? $params->title : $name,
+ isset($params->description) ? $params->description : null);
+ break;
- if ($new_values->validate() && $item_id != 1) {
- $errors = gallery_rest::_check_for_conflicts($parent_id, $item_id,
- $new_values["name"], $new_values["slug"]);
- if (!empty($errors)) {
- !empty($errors["name_conflict"]) OR $new_values->add_error("name", "Duplicate name");
- !empty($errors["slug_conflict"]) OR
- $new_values->add_error("slug", "Duplicate Internet address");
- }
+ default:
+ throw new Rest_Exception("Invalid type: $args->type", 400);
}
- return $new_values;
+ return rest::reply(array("url" => url::abs_site("/rest/gallery/" . $item->relative_url())));
}
- private static function _check_for_conflicts($parent_id, $item_id, $new_name, $new_slug) {
- $errors = array();
-
- if ($row = db::build()
- ->select(array("name", "slug"))
- ->from("items")
- ->where("parent_id", "=", $parent_id)
- ->where("id", "<>", $item_id)
- ->and_open()
- ->where("name", "=", $new_name)
- ->or_where("slug", "=", $new_slug)
- ->close()
- ->execute()
- ->current()) {
- if ($row->name == $new_name) {
- $errors["name_conflict"] = 1;
- }
- if ($row->slug == $new_slug) {
- $errors["slug_conflict"] = 1;
- }
- }
+ static function delete($request) {
+ $item = rest::resolve($request->url);
+ access::required("edit", $item);
+
+ $item->delete();
+ return rest::reply();
+ }
- return $errors;
+ static function resolve($path) {
+ return url::get_item_from_uri($path);
}
}
diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php
index f6181f8a..53291ccc 100644
--- a/modules/gallery/helpers/item.php
+++ b/modules/gallery/helpers/item.php
@@ -78,24 +78,6 @@ class item_Core {
graphics::generate($album);
}
- static function validate_no_slashes($input) {
- if (strpos($input->value, "/") !== false) {
- $input->add_error("no_slashes", 1);
- }
- }
-
- static function validate_no_trailing_period($input) {
- if (rtrim($input->value, ".") !== $input->value) {
- $input->add_error("no_trailing_period", 1);
- }
- }
-
- static function validate_url_safe($input) {
- if (preg_match("/[^A-Za-z0-9-_]/", $input->value)) {
- $input->add_error("not_url_safe", 1);
- }
- }
-
/**
* Sanitize a filename into something presentable as an item title
* @param string $filename
@@ -173,4 +155,20 @@ class item_Core {
static function root() {
return model_cache::get("item", 1);
}
+
+ /**
+ * Return a query to get a random Item_Model, with optional filters
+ *
+ * @param array (optional) where tuple
+ */
+ static function random_query($where=null) {
+ // Pick a random number and find the item that's got nearest smaller number.
+ // This approach works best when the random numbers in the system are roughly evenly
+ // distributed so this is going to be more efficient with larger data sets.
+ return ORM::factory("item")
+ ->viewable()
+ ->where("rand_key", "<", ((float)mt_rand()) / (float)mt_getrandmax())
+ ->merge_where($where)
+ ->order_by("rand_key", "DESC");
+ }
} \ No newline at end of file
diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php
index 01859924..b0d24f68 100644
--- a/modules/gallery/helpers/movie.php
+++ b/modules/gallery/helpers/movie.php
@@ -85,7 +85,6 @@ class movie_Core {
$movie->resize_dirty = 1;
$movie->sort_column = "weight";
$movie->slug = $slug;
- $movie->rand_key = ((float)mt_rand()) / (float)mt_getrandmax();
// Randomize the name if there's a conflict
// @todo Improve this. Random numbers are not user friendly
diff --git a/modules/gallery/helpers/photo.php b/modules/gallery/helpers/photo.php
index 4e20e610..aeae7f56 100644
--- a/modules/gallery/helpers/photo.php
+++ b/modules/gallery/helpers/photo.php
@@ -84,7 +84,6 @@ class photo_Core {
$photo->resize_dirty = 1;
$photo->sort_column = "weight";
$photo->slug = $slug;
- $photo->rand_key = ((float)mt_rand()) / (float)mt_getrandmax();
// Randomize the name or slug if there's a conflict
// @todo Improve this. Random numbers are not user friendly
diff --git a/modules/gallery/libraries/ORM_MPTT.php b/modules/gallery/libraries/ORM_MPTT.php
index 0ea519c9..404d61ff 100644
--- a/modules/gallery/libraries/ORM_MPTT.php
+++ b/modules/gallery/libraries/ORM_MPTT.php
@@ -40,43 +40,45 @@ class ORM_MPTT_Core extends ORM {
}
/**
- * Add this node as a child of the parent provided.
+ * Overload ORM::save() to update the MPTT tree when we add new items to the hierarchy.
*
* @chainable
- * @param integer $parent_id the id of the parent node
- * @return ORM
+ * @return ORM
*/
- function add_to_parent($parent) {
- $this->lock();
- $parent->reload(); // Assume that the prior lock holder may have changed the parent
-
- try {
- // Make a hole in the parent for this new item
- $this->db_builder
- ->update($this->table_name)
- ->set("left_ptr", new Database_Expression("`left_ptr` + 2"))
- ->where("left_ptr", ">=", $parent->right_ptr)
- ->execute();
- $this->db_builder
- ->update($this->table_name)
- ->set("right_ptr", new Database_Expression("`right_ptr` + 2"))
- ->where("right_ptr", ">=", $parent->right_ptr)
- ->execute();
- $parent->right_ptr += 2;
+ function save() {
+ if (!$this->loaded()) {
+ $this->lock();
+ $parent = ORM::factory("item")->where("id", "=", $this->parent_id)->find();
- // Insert this item into the hole
- $this->left_ptr = $parent->right_ptr - 2;
- $this->right_ptr = $parent->right_ptr - 1;
- $this->parent_id = $parent->id;
- $this->level = $parent->level + 1;
- $this->save();
- $parent->reload();
- } catch (Exception $e) {
+ try {
+ // Make a hole in the parent for this new item
+ $this->db_builder
+ ->update($this->table_name)
+ ->set("left_ptr", new Database_Expression("`left_ptr` + 2"))
+ ->where("left_ptr", ">=", $parent->right_ptr)
+ ->execute();
+ $this->db_builder
+ ->update($this->table_name)
+ ->set("right_ptr", new Database_Expression("`right_ptr` + 2"))
+ ->where("right_ptr", ">=", $parent->right_ptr)
+ ->execute();
+ $parent->right_ptr += 2;
+
+ // Insert this item into the hole
+ $this->left_ptr = $parent->right_ptr - 2;
+ $this->right_ptr = $parent->right_ptr - 1;
+ $this->parent_id = $parent->id;
+ $this->level = $parent->level + 1;
+ } catch (Exception $e) {
+ $this->unlock();
+ throw $e;
+ }
+ parent::save();
$this->unlock();
- throw $e;
+ } else {
+ parent::save();
}
- $this->unlock();
return $this;
}
@@ -165,11 +167,8 @@ class ORM_MPTT_Core extends ORM {
* @return array ORM
*/
function children($limit=null, $offset=null, $where=null, $order_by=array("id" => "ASC")) {
- if ($where) {
- $this->merge_where($where);
- }
-
return $this
+ ->merge_where($where)
->where("parent_id", "=", $this->id)
->order_by($order_by)
->find_all($limit, $offset);
@@ -183,11 +182,8 @@ class ORM_MPTT_Core extends ORM {
* @return array ORM
*/
function children_count($where=null) {
- if ($where) {
- $this->merge_where($where);
- }
-
return $this
+ ->merge_where($where)
->where("parent_id", "=", $this->id)
->count_all();
}
@@ -202,11 +198,8 @@ class ORM_MPTT_Core extends ORM {
* @return object ORM_Iterator
*/
function descendants($limit=null, $offset=null, $where=null, $order_by=array("id" => "ASC")) {
- if ($where) {
- $this->merge_where($where);
- }
-
return $this
+ ->merge_where($where)
->where("left_ptr", ">", $this->left_ptr)
->where("right_ptr", "<=", $this->right_ptr)
->order_by($order_by)
@@ -220,11 +213,8 @@ class ORM_MPTT_Core extends ORM {
* @return integer child count
*/
function descendants_count($where=null) {
- if ($where) {
- $this->merge_where($where);
- }
-
return $this
+ ->merge_where($where)
->where("left_ptr", ">", $this->left_ptr)
->where("right_ptr", "<=", $this->right_ptr)
->count_all();
diff --git a/modules/gallery/models/item.php b/modules/gallery/models/item.php
index 6851e1a3..46b0304e 100644
--- a/modules/gallery/models/item.php
+++ b/modules/gallery/models/item.php
@@ -21,11 +21,13 @@ class Item_Model extends ORM_MPTT {
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]"
+ var $rules = array(
+ "name" => array("rules" => array("length[0,255]", "required")),
+ "title" => array("rules" => array("length[0,255]", "required")),
+ "slug" => array("rules" => array("length[0,255]", "required")),
+ "description" => array("rules" => array("length[0,65535]")),
+ "parent_id" => array("rules" => array("Item_Model::valid_parent")),
+ "type" => array("rules" => array("Item_Model::valid_type")),
);
/**
@@ -146,21 +148,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 +171,6 @@ 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;
}
@@ -376,30 +357,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 an item.
* @see ORM::save()
+ *
+ * @return ORM Item_Model
*/
public function save() {
$significant_changes = $this->changed;
@@ -410,18 +371,83 @@ 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;
+ }
+ if (empty($this->slug)) {
+ $tmp = pathinfo($this->name, PATHINFO_FILENAME);
+ $tmp = preg_replace("/[^A-Za-z0-9-_]+/", "-", $tmp);
+ $this->slug = trim($tmp, "-");
+ }
+
+ // Randomize the name or slug if there's a conflict
+ // @todo Improve this. Random numbers are not user friendly
+ $base_name = $this->name;
+ $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();
+ $this->name = "$base_name-$rand";
+ $this->slug = "$base_slug-$rand";
+ }
+
+ parent::save();
+
+ // Call this after we finish saving so that the paths are correct.
+ if ($this->is_album()) {
+ mkdir($this->file_path());
+ mkdir(dirname($this->thumb_path()));
+ mkdir(dirname($this->resize_path()));
+ }
+
+ 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);
}
}
- $original = clone $this->original();
- parent::save();
- if (isset($send_event)) {
- module::event("item_updated", $original, $this);
- }
return $this;
}
@@ -657,4 +683,76 @@ class Item_Model extends ORM_MPTT {
}
return parent::descendants($limit, $offset, $where, $order_by);
}
+
+ /**
+ * Add some custom per-instance rules.
+ */
+ public function validate($array=null) {
+ if (!$array) {
+ // The root item has different rules for the name and slug.
+ if ($this->id == 1) {
+ $this->rules["name"]["rules"][] = "length[0]";
+ $this->rules["slug"]["rules"][] = "length[0]";
+ }
+
+ // Names and slugs can't conflict
+ $this->rules["name"]["callbacks"][] = array($this, "valid_name");
+ $this->rules["slug"]["callbacks"][] = array($this, "valid_slug");
+ }
+
+ parent::validate($array);
+ }
+
+ /**
+ * Validate that the desired slug does not conflict.
+ */
+ public function valid_slug(Validation $v, $value) {
+ if (preg_match("/[^A-Za-z0-9-_]/", $value)) {
+ $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", "=", $value)
+ ->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, $value) {
+ if (strpos($value, "/") !== false) {
+ $v->add_error("name", "no_slashes");
+ } else if (rtrim($value, ".") !== $value) {
+ $v->add_error("name", "no_trailing_period");
+ } else if (db::build()
+ ->from("items")
+ ->where("parent_id", "=", $this->parent_id)
+ ->where("id", "<>", $this->id)
+ ->where("name", "=", $value)
+ ->count_records()) {
+ $v->add_error("name", "conflict");
+ }
+ }
+
+ /**
+ * Make sure that the type is valid.
+ */
+ static function valid_type($value) {
+ return in_array($value, array("album", "photo", "movie"));
+ }
+
+ /**
+ * Make sure that the parent id refers to an album.
+ */
+ static function valid_parent($value) {
+ return db::build()
+ ->from("items")
+ ->where("id", "=", $value)
+ ->where("type", "=", "album")
+ ->count_records() == 1;
+ }
}
diff --git a/modules/gallery/tests/Gallery_Rest_Helper_Test.php b/modules/gallery/tests/Gallery_Rest_Helper_Test.php
index cd0aabae..c5c8a890 100644
--- a/modules/gallery/tests/Gallery_Rest_Helper_Test.php
+++ b/modules/gallery/tests/Gallery_Rest_Helper_Test.php
@@ -136,7 +136,8 @@ class Gallery_Rest_Helper_Test extends Unit_Test_Case {
try {
gallery_rest::put($request);
} catch (Rest_Exception $e) {
- $this->assert_equal("400 Bad request", $e->getMessage());
+ $this->assert_equal("Bad request", $e->getMessage());
+ $this->assert_equal(400, $e->getCode());
} catch (Exception $e) {
$this->assert_false(true, $e->__toString());
}