2013-04-04 09:26:00 +00:00
|
|
|
<?php defined("SYSPATH") or die("No direct script access.");
|
|
|
|
/**
|
|
|
|
* Gallery - a web based photo album viewer and editor
|
2013-04-04 09:26:04 +00:00
|
|
|
* Copyright (C) 2000-2012 Bharat Mediratta
|
2013-04-04 09:26:00 +00:00
|
|
|
*
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$source->parent_id = $target->id;
|
|
|
|
|
|
|
|
// Moving may result in name or slug conflicts. If that happens, try up to 5 times to pick a
|
|
|
|
// random name (or slug) to avoid the conflict.
|
|
|
|
$orig_name = $source->name;
|
|
|
|
$orig_name_filename = pathinfo($source->name, PATHINFO_FILENAME);
|
|
|
|
$orig_name_extension = pathinfo($source->name, PATHINFO_EXTENSION);
|
|
|
|
$orig_slug = $source->slug;
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
|
|
try {
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
} catch (ORM_Validation_Exception $e) {
|
|
|
|
$rand = rand(10, 99);
|
|
|
|
$errors = $e->validation->errors();
|
|
|
|
if (isset($errors["name"])) {
|
|
|
|
$source->name = $orig_name_filename . "-{$rand}." . $orig_name_extension;
|
|
|
|
unset($errors["name"]);
|
|
|
|
}
|
|
|
|
if (isset($errors["slug"])) {
|
|
|
|
$source->slug = $orig_slug . "-{$rand}";
|
|
|
|
unset($errors["slug"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($errors) {
|
|
|
|
// There were other validation issues-- we don't know how to handle those
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
model_cache::clear();
|
|
|
|
$parent->album_cover_item_id = $item->is_album() ? $item->album_cover_item_id : $item->id;
|
|
|
|
if ($item->thumb_dirty) {
|
|
|
|
$parent->thumb_dirty = 1;
|
|
|
|
graphics::generate($parent);
|
|
|
|
} else {
|
|
|
|
copy($item->thumb_path(), $parent->thumb_path());
|
|
|
|
$parent->thumb_width = $item->thumb_width;
|
|
|
|
$parent->thumb_height = $item->thumb_height;
|
|
|
|
}
|
|
|
|
$parent->save();
|
|
|
|
$grand_parent = $parent->parent();
|
|
|
|
if ($grand_parent && access::can("edit", $grand_parent) &&
|
|
|
|
$grand_parent->album_cover_item_id == null) {
|
|
|
|
item::make_album_cover($parent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static function remove_album_cover($album) {
|
|
|
|
access::required("view", $album);
|
|
|
|
access::required("edit", $album);
|
|
|
|
@unlink($album->thumb_path());
|
|
|
|
|
|
|
|
model_cache::clear();
|
|
|
|
$album->album_cover_item_id = null;
|
|
|
|
$album->thumb_width = 0;
|
|
|
|
$album->thumb_height = 0;
|
|
|
|
$album->thumb_dirty = 1;
|
|
|
|
$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) {
|
2013-04-04 09:26:02 +00:00
|
|
|
$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);
|
|
|
|
}
|
2013-04-04 09:26:00 +00:00
|
|
|
$result = preg_replace("/[^A-Za-z0-9-_]+/", "-", $result);
|
2013-04-04 09:26:02 +00:00
|
|
|
$result = preg_replace("/-+/", "-", $result);
|
2013-04-04 09:26:00 +00:00
|
|
|
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;
|
|
|
|
}
|
2013-04-04 09:26:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2013-04-04 09:26:00 +00:00
|
|
|
}
|