diff options
Diffstat (limited to 'modules/gallery/helpers/item.php')
| -rw-r--r-- | modules/gallery/helpers/item.php | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php new file mode 100644 index 0000000..9882a9c --- /dev/null +++ b/modules/gallery/helpers/item.php @@ -0,0 +1,433 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class item_Core { + static function move($source, $target) { + access::required("view", $source); + access::required("view", $target); + access::required("edit", $source); + access::required("edit", $target); + + $parent = $source->parent(); + if ($parent->album_cover_item_id == $source->id) { + if ($parent->children_count() > 1) { + foreach ($parent->children(2) as $child) { + if ($child->id != $source->id) { + $new_cover_item = $child; + break; + } + } + item::make_album_cover($new_cover_item); + } else { + item::remove_album_cover($parent); + } + } + + $orig_name = $source->name; + $source->parent_id = $target->id; + $source->save(); + if ($orig_name != $source->name) { + switch ($source->type) { + case "album": + message::info( + t("Album <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "photo": + message::info( + t("Photo <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "movie": + message::info( + t("Movie <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + } + } + + // If the target has no cover item, make this it. + if ($target->album_cover_item_id == null) { + item::make_album_cover($source); + } + } + + static function make_album_cover($item) { + $parent = $item->parent(); + access::required("view", $item); + access::required("view", $parent); + access::required("edit", $parent); + + $old_album_cover_id = $parent->album_cover_item_id; + + model_cache::clear(); + $parent->album_cover_item_id = $item->is_album() ? $item->album_cover_item_id : $item->id; + $parent->save(); + graphics::generate($parent); + + // Walk up the parent hierarchy and set album covers if necessary + $grand_parent = $parent->parent(); + if ($grand_parent && access::can("edit", $grand_parent) && + $grand_parent->album_cover_item_id == null) { + item::make_album_cover($parent); + } + + // When albums are album covers themselves, we hotlink directly to the target item. This + // means that when we change an album cover, the grandparent may have a deep link to the old + // album cover. So find any parent albums that had the old item as their album cover and + // switch them over to the new item. + if ($old_album_cover_id) { + foreach ($item->parents(array(array("album_cover_item_id", "=", $old_album_cover_id))) + as $ancestor) { + if (access::can("edit", $ancestor)) { + $ancestor->album_cover_item_id = $parent->album_cover_item_id; + $ancestor->save(); + graphics::generate($ancestor); + } + } + } + } + + static function remove_album_cover($album) { + access::required("view", $album); + access::required("edit", $album); + + model_cache::clear(); + $album->album_cover_item_id = null; + $album->save(); + graphics::generate($album); + } + + /** + * Sanitize a filename into something presentable as an item title + * @param string $filename + * @return string title + */ + static function convert_filename_to_title($filename) { + $title = strtr($filename, "_", " "); + $title = preg_replace("/\..{3,4}$/", "", $title); + $title = preg_replace("/ +/", " ", $title); + return $title; + } + + /** + * Convert a filename into something we can use as a url component. + * @param string $filename + */ + static function convert_filename_to_slug($filename) { + $result = str_replace("&", "-and-", $filename); + $result = str_replace(" ", "-", $result); + + // It's not easy to extend the text helper since it's called by the Input class which is + // referenced in hooks/init_gallery, so it's + if (class_exists("transliterate")) { + $result = transliterate::utf8_to_ascii($result); + } else { + $result = text::transliterate_to_ascii($result); + } + $result = preg_replace("/[^A-Za-z0-9-_]+/", "-", $result); + $result = preg_replace("/-+/", "-", $result); + return trim($result, "-"); + } + + /** + * Display delete confirmation message and form + * @param object $item + * @return string form + */ + static function get_delete_form($item) { + $page_type = Input::instance()->get("page_type"); + $from_id = Input::instance()->get("from_id"); + $form = new Forge( + "quick/delete/$item->id?page_type=$page_type&from_id=$from_id", "", + "post", array("id" => "g-confirm-delete")); + $group = $form->group("confirm_delete")->label(t("Confirm Deletion")); + $group->submit("")->value(t("Delete")); + $form->script("") + ->url(url::abs_file("modules/gallery/js/item_form_delete.js")); + return $form; + } + + /** + * Get the next weight value + */ + static function get_max_weight() { + // Guard against an empty result when we create the first item. It's unfortunate that we + // have to check this every time. + // @todo: figure out a better way to bootstrap the weight. + $result = db::build() + ->select("weight")->from("items") + ->order_by("weight", "desc")->limit(1) + ->execute()->current(); + return ($result ? $result->weight : 0) + 1; + } + + /** + * Add a set of restrictions to any following queries to restrict access only to items + * viewable by the active user. + * @chainable + */ + static function viewable($model) { + $view_restrictions = array(); + if (!identity::active_user()->admin) { + foreach (identity::group_ids_for_active_user() as $id) { + $view_restrictions[] = array("items.view_$id", "=", access::ALLOW); + } + } + + if (count($view_restrictions)) { + $model->and_open()->merge_or_where($view_restrictions)->close(); + } + + return $model; + } + + /** + * Find an item by its path. If there's no match, return an empty Item_Model. + * NOTE: the caller is responsible for performing security checks on the resulting item. + * @param string $path + * @return object Item_Model + */ + static function find_by_path($path) { + $path = trim($path, "/"); + + // The root path name is NULL not "", hence this workaround. + if ($path == "") { + return item::root(); + } + + // Check to see if there's an item in the database with a matching relative_path_cache value. + // Since that field is urlencoded, we must urlencoded the components of the path. + foreach (explode("/", $path) as $part) { + $encoded_array[] = rawurlencode($part); + } + $encoded_path = join("/", $encoded_array); + $item = ORM::factory("item") + ->where("relative_path_cache", "=", $encoded_path) + ->find(); + if ($item->loaded()) { + return $item; + } + + // Since the relative_path_cache field is a cache, it can be unavailable. If we don't find + // anything, fall back to checking the path the hard way. + $paths = explode("/", $path); + foreach (ORM::factory("item") + ->where("name", "=", end($paths)) + ->where("level", "=", count($paths) + 1) + ->find_all() as $item) { + if (urldecode($item->relative_path()) == $path) { + return $item; + } + } + + return new Item_Model(); + } + + + /** + * Locate an item using the URL. We assume that the url is in the form /a/b/c where each + * component matches up with an item slug. If there's no match, return an empty Item_Model + * NOTE: the caller is responsible for performing security checks on the resulting item. + * @param string $url the relative url fragment + * @return Item_Model + */ + static function find_by_relative_url($relative_url) { + // In most cases, we'll have an exact match in the relative_url_cache item field. + // but failing that, walk down the tree until we find it. The fallback code will fix caches + // as it goes, so it'll never be run frequently. + $item = ORM::factory("item")->where("relative_url_cache", "=", $relative_url)->find(); + if (!$item->loaded()) { + $segments = explode("/", $relative_url); + foreach (ORM::factory("item") + ->where("slug", "=", end($segments)) + ->where("level", "=", count($segments) + 1) + ->find_all() as $match) { + if ($match->relative_url() == $relative_url) { + $item = $match; + } + } + } + return $item; + } + + /** + * Return the root Item_Model + * @return Item_Model + */ + static function root() { + return model_cache::get("item", 1); + } + + /** + * Return a query to get a random Item_Model, with optional filters. + * Usage: item::random_query()->execute(); + * + * Note: You can add your own ->where() clauses but if your Gallery is + * small or your where clauses are over-constrained you may wind up with + * no item. You should try running this a few times in a loop if you + * don't get an item back. + */ + static function random_query() { + // 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", "<", random::percent()) + ->order_by("rand_key", "DESC"); + } + + /** + * Find the position of the given item in its parent album. The resulting + * value is 1-indexed, so the first child in the album is at position 1. + * + * @param Item_Model $item + * @param array $where an array of arrays, each compatible with ORM::where() + */ + static function get_position($item, $where=array()) { + $album = $item->parent(); + + if (!strcasecmp($album->sort_order, "DESC")) { + $comp = ">"; + } else { + $comp = "<"; + } + $query_model = ORM::factory("item"); + + // If the comparison column has NULLs in it, we can't use comparators on it + // and will have to deal with it the hard way. + $count = $query_model->viewable() + ->where("parent_id", "=", $album->id) + ->where($album->sort_column, "IS", null) + ->merge_where($where) + ->count_all(); + + if (empty($count)) { + // There are no NULLs in the sort column, so we can just use it directly. + $sort_column = $album->sort_column; + + $position = $query_model->viewable() + ->where("parent_id", "=", $album->id) + ->where($sort_column, $comp, $item->$sort_column) + ->merge_where($where) + ->count_all(); + + // We stopped short of our target value in the sort (notice that we're + // using a inequality comparator above) because it's possible that we have + // duplicate values in the sort column. An equality check would just + // arbitrarily pick one of those multiple possible equivalent columns, + // which would mean that if you choose a sort order that has duplicates, + // it'd pick any one of them as the child's "position". + // + // Fix this by doing a 2nd query where we iterate over the equivalent + // columns and add them to our position count. + foreach ($query_model->viewable() + ->select("id") + ->where("parent_id", "=", $album->id) + ->where($sort_column, "=", $item->$sort_column) + ->merge_where($where) + ->order_by(array("id" => "ASC")) + ->find_all() as $row) { + $position++; + if ($row->id == $item->id) { + break; + } + } + } else { + // There are NULLs in the sort column, so we can't use MySQL comparators. + // Fall back to iterating over every child row to get to the current one. + // This can be wildly inefficient for really large albums, but it should + // be a rare case that the user is sorting an album with null values in + // the sort column. + // + // Reproduce the children() functionality here using Database directly to + // avoid loading the whole ORM for each row. + $order_by = array($album->sort_column => $album->sort_order); + // Use id as a tie breaker + if ($album->sort_column != "id") { + $order_by["id"] = "ASC"; + } + + $position = 0; + foreach ($query_model->viewable() + ->select("id") + ->where("parent_id", "=", $album->id) + ->merge_where($where) + ->order_by($order_by) + ->find_all() as $row) { + $position++; + if ($row->id == $item->id) { + break; + } + } + } + + return $position; + } + + /** + * Set the display context callback for any future item renders. + */ + static function set_display_context_callback() { + if (!request::user_agent("robot")) { + $args = func_get_args(); + Cache::instance()->set("display_context_" . $sid = Session::instance()->id(), $args, + array("display_context")); + } + } + + /** + * Get rid of the display context callback + */ + static function clear_display_context_callback() { + Cache::instance()->delete("display_context_" . $sid = Session::instance()->id()); + } + + /** + * Call the display context callback for the given item + */ + static function get_display_context($item) { + if (!request::user_agent("robot")) { + $args = Cache::instance()->get("display_context_" . $sid = Session::instance()->id()); + $callback = $args[0]; + $args[0] = $item; + } + + if (empty($callback)) { + $callback = "Albums_Controller::get_display_context"; + $args = array($item); + } + return call_user_func_array($callback, $args); + } + + /** + * Reset all child weights of a given album to a monotonically increasing sequence based on the + * current sort order of the album. + */ + static function resequence_child_weights($album) { + $weight = 0; + foreach ($album->children() as $child) { + $child->weight = ++$weight; + $child->save(); + } + } +}
\ No newline at end of file |
