summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.build_number2
-rw-r--r--index.php9
-rw-r--r--installer/install.sql8
-rw-r--r--installer/web.php4
-rw-r--r--modules/comment/helpers/comment_installer.php1
-rw-r--r--modules/digibug/helpers/digibug_installer.php1
-rw-r--r--modules/exif/helpers/exif_installer.php1
-rw-r--r--modules/g2_import/controllers/g2.php12
-rw-r--r--modules/g2_import/helpers/g2_import_installer.php1
-rw-r--r--modules/gallery/controllers/albums.php6
-rw-r--r--modules/gallery/controllers/file_proxy.php7
-rw-r--r--modules/gallery/controllers/packager.php9
-rw-r--r--modules/gallery/controllers/upgrader.php6
-rw-r--r--modules/gallery/controllers/uploader.php6
-rw-r--r--modules/gallery/helpers/data_rest.php25
-rw-r--r--modules/gallery/helpers/gallery.php16
-rw-r--r--modules/gallery/helpers/gallery_event.php40
-rw-r--r--modules/gallery/helpers/gallery_graphics.php147
-rw-r--r--modules/gallery/helpers/gallery_installer.php66
-rw-r--r--modules/gallery/helpers/gallery_task.php115
-rw-r--r--modules/gallery/helpers/gallery_theme.php4
-rw-r--r--modules/gallery/helpers/graphics.php195
-rw-r--r--modules/gallery/helpers/item.php107
-rw-r--r--modules/gallery/helpers/item_rest.php2
-rw-r--r--modules/gallery/helpers/legal_file.php219
-rw-r--r--modules/gallery/helpers/module.php3
-rw-r--r--modules/gallery/helpers/movie.php125
-rw-r--r--modules/gallery/helpers/photo.php70
-rw-r--r--modules/gallery/helpers/system.php36
-rw-r--r--modules/gallery/images/missing_album_cover.jpgbin0 -> 4453 bytes
-rw-r--r--modules/gallery/images/missing_photo.jpgbin0 -> 2034 bytes
-rw-r--r--modules/gallery/images/missing_photo.pngbin1570 -> 0 bytes
-rw-r--r--modules/gallery/js/l10n_client.js6
-rw-r--r--modules/gallery/libraries/Form_Uploadify.php2
-rw-r--r--modules/gallery/libraries/Gallery_View.php6
-rw-r--r--modules/gallery/libraries/MY_Database.php13
-rw-r--r--modules/gallery/libraries/ORM_MPTT.php5
-rw-r--r--modules/gallery/libraries/Theme_View.php6
-rw-r--r--modules/gallery/libraries/drivers/Cache/Database.php4
-rw-r--r--modules/gallery/models/item.php329
-rw-r--r--modules/gallery/module.info2
-rw-r--r--modules/gallery/tests/Data_Rest_Helper_Test.php9
-rw-r--r--modules/gallery/tests/Database_Test.php6
-rw-r--r--modules/gallery/tests/File_Structure_Test.php48
-rw-r--r--modules/gallery/tests/Gallery_Filters.php6
-rw-r--r--modules/gallery/tests/Gallery_Graphics_Helper_Test.php137
-rw-r--r--modules/gallery/tests/Graphics_Helper_Test.php158
-rw-r--r--modules/gallery/tests/Item_Helper_Test.php18
-rw-r--r--modules/gallery/tests/Item_Model_Test.php335
-rw-r--r--modules/gallery/tests/Legal_File_Helper_Test.php111
-rw-r--r--modules/gallery/tests/Movie_Helper_Test.php56
-rw-r--r--modules/gallery/tests/Photo_Helper_Test.php70
-rw-r--r--modules/gallery/tests/controller_auth_data.txt1
-rw-r--r--modules/gallery/tests/xss_data.txt28
-rw-r--r--modules/gallery/views/movieplayer.html.php2
-rw-r--r--modules/gallery_unit_test/controllers/gallery_unit_test.php2
-rw-r--r--modules/gallery_unit_test/helpers/test.php28
-rw-r--r--modules/image_block/helpers/image_block_installer.php1
-rw-r--r--modules/info/helpers/info_block.php2
-rw-r--r--modules/info/helpers/info_installer.php1
-rw-r--r--modules/notification/helpers/notification_installer.php2
-rw-r--r--modules/organize/controllers/organize.php32
-rw-r--r--modules/organize/views/organize_frame.html.php74
-rw-r--r--modules/rest/helpers/rest.php2
-rw-r--r--modules/rest/helpers/rest_installer.php1
-rw-r--r--modules/search/controllers/search.php25
-rw-r--r--modules/search/helpers/search.php10
-rw-r--r--modules/search/helpers/search_installer.php1
-rw-r--r--modules/server_add/controllers/server_add.php8
-rw-r--r--modules/server_add/helpers/server_add_installer.php1
-rw-r--r--modules/slideshow/helpers/slideshow_installer.php1
-rw-r--r--modules/tag/controllers/tag.php1
-rw-r--r--modules/tag/controllers/tags.php2
-rw-r--r--modules/tag/helpers/tag_installer.php1
-rw-r--r--modules/user/helpers/user_installer.php1
-rw-r--r--modules/watermark/controllers/admin_watermarks.php43
-rw-r--r--modules/watermark/helpers/watermark_installer.php1
-rw-r--r--modules/watermark/tests/Admin_Watermarks_Controller_Test.php124
-rw-r--r--system/helpers/url.php2
-rw-r--r--system/libraries/ORM.php1
-rw-r--r--themes/admin_wind/css/fix-ie.css2
81 files changed, 2359 insertions, 611 deletions
diff --git a/.build_number b/.build_number
index 87f48779..5a6b0671 100644
--- a/.build_number
+++ b/.build_number
@@ -3,4 +3,4 @@
; process. You don't need to edit it. In fact..
;
; DO NOT EDIT THIS FILE BY HAND!
-build_number=280
+build_number=341
diff --git a/index.php b/index.php
index 9ff8e3b4..c39452ea 100644
--- a/index.php
+++ b/index.php
@@ -29,6 +29,13 @@ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
exit("Gallery is not supported on Windows (PHP reports that you're using: " . PHP_OS . ")");
}
+// Gallery doesn't use Zend Guard code obfuscation, and Kohana 2.4 will not work if level is 3+.
+if (function_exists("zend_current_obfuscation_level") && (zend_current_obfuscation_level() >= 3)) {
+ exit("Gallery doesn't use Zend Guard code obfuscation, and is incompatible if it's running " .
+ "with a level of 3 or higher. For Gallery to run, please edit your main php.ini file and " .
+ "change/add the following line: 'zend_loader.obfuscation_level_support = 2'");
+}
+
// PHP 5.4 requires a timezone - if one isn't set date functions aren't going to work properly.
// We'll log this once the logging system is initialized (in the gallery_event::gallery_ready).
if (!ini_get("date.timezone")) {
@@ -106,7 +113,7 @@ if (PHP_SAPI == "cli") {
define("TEST_MODE", 0);
define("VARPATH", realpath("var") . "/");
}
-define("TMPPATH", VARPATH . "/tmp/");
+define("TMPPATH", VARPATH . "tmp/");
if (file_exists("local.php")) {
include("local.php");
diff --git a/installer/install.sql b/installer/install.sql
index b01c5a7c..b89d6b9b 100644
--- a/installer/install.sql
+++ b/installer/install.sql
@@ -186,7 +186,8 @@ CREATE TABLE {items} (
KEY `type` (`type`),
KEY `random` (`rand_key`),
KEY `weight` (`weight`),
- KEY `left_ptr` (`left_ptr`)
+ KEY `left_ptr` (`left_ptr`),
+ KEY `relative_path_cache` (`relative_path_cache`)
) AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO {items} VALUES (1,NULL,NULL,UNIX_TIMESTAMP(),'',NULL,1,1,NULL,NULL,2,0,NULL,'','',1,NULL,NULL,2,NULL,'weight','ASC',1,NULL,NULL,'Gallery','album',UNIX_TIMESTAMP(),0,1,NULL,'1','1');
@@ -244,7 +245,7 @@ CREATE TABLE {modules} (
KEY `weight` (`weight`)
) AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO {modules} VALUES (1,1,'gallery',53,1);
+INSERT INTO {modules} VALUES (1,1,'gallery',56,1);
INSERT INTO {modules} VALUES (2,1,'user',4,2);
INSERT INTO {modules} VALUES (3,1,'comment',7,3);
INSERT INTO {modules} VALUES (4,1,'organize',4,4);
@@ -382,7 +383,7 @@ CREATE TABLE {vars} (
`value` text,
PRIMARY KEY (`id`),
UNIQUE KEY `module_name` (`module_name`,`name`)
-) AUTO_INCREMENT=46 DEFAULT CHARSET=utf8;
+) AUTO_INCREMENT=47 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO {vars} VALUES (NULL,'gallery','active_site_theme','wind');
INSERT INTO {vars} VALUES (NULL,'gallery','active_admin_theme','admin_wind');
@@ -416,6 +417,7 @@ INSERT INTO {vars} VALUES (NULL,'gallery','extra_binary_paths','/usr/local/bin:/
INSERT INTO {vars} VALUES (NULL,'gallery','timezone',NULL);
INSERT INTO {vars} VALUES (NULL,'gallery','lock_timeout','1');
INSERT INTO {vars} VALUES (NULL,'gallery','movie_extract_frame_time','3');
+INSERT INTO {vars} VALUES (NULL,'gallery','movie_allow_uploads','autodetect');
INSERT INTO {vars} VALUES (NULL,'gallery','blocks_site_sidebar','a:4:{i:10;a:2:{i:0;s:7:\"gallery\";i:1;s:8:\"language\";}i:11;a:2:{i:0;s:4:\"info\";i:1;s:8:\"metadata\";}i:12;a:2:{i:0;s:3:\"rss\";i:1;s:9:\"rss_feeds\";}i:13;a:2:{i:0;s:3:\"tag\";i:1;s:3:\"tag\";}}');
INSERT INTO {vars} VALUES (NULL,'gallery','identity_provider','user');
INSERT INTO {vars} VALUES (NULL,'user','minimum_password_length','5');
diff --git a/installer/web.php b/installer/web.php
index a7060bd8..5fa8541e 100644
--- a/installer/web.php
+++ b/installer/web.php
@@ -41,9 +41,9 @@ if (installer::already_installed()) {
list ($config["host"], $config["port"]) = explode(":", $config["host"] . ":");
foreach ($config as $k => $v) {
if ($k == "password") {
- $config[$k] = str_replace("'", "\\'", $v);
+ $config[$k] = str_replace(array("'", "\\"), array("\\'", "\\\\"), $v);
} else {
- $config[$k] = strtr($v, "'`", "__");
+ $config[$k] = strtr($v, "'`\\", "___");
}
}
diff --git a/modules/comment/helpers/comment_installer.php b/modules/comment/helpers/comment_installer.php
index 6dbd31cf..136f96ef 100644
--- a/modules/comment/helpers/comment_installer.php
+++ b/modules/comment/helpers/comment_installer.php
@@ -49,7 +49,6 @@ class comment_installer {
module::set_var("comment", "spam_caught", 0);
module::set_var("comment", "access_permissions", "everybody");
module::set_var("comment", "rss_visible", "all");
- module::set_version("comment", 7);
}
static function upgrade($version) {
diff --git a/modules/digibug/helpers/digibug_installer.php b/modules/digibug/helpers/digibug_installer.php
index b2e529d7..be88b5ec 100644
--- a/modules/digibug/helpers/digibug_installer.php
+++ b/modules/digibug/helpers/digibug_installer.php
@@ -30,7 +30,6 @@ class digibug_installer {
module::set_var("digibug", "company_id", "3153");
module::set_var("digibug", "event_id", "8491");
- module::set_version("digibug", 2);
}
static function upgrade($version) {
diff --git a/modules/exif/helpers/exif_installer.php b/modules/exif/helpers/exif_installer.php
index f4c2aa3b..75d0f835 100644
--- a/modules/exif/helpers/exif_installer.php
+++ b/modules/exif/helpers/exif_installer.php
@@ -29,7 +29,6 @@ class exif_installer {
PRIMARY KEY (`id`),
KEY(`item_id`))
DEFAULT CHARSET=utf8;");
- module::set_version("exif", 1);
}
static function activate() {
diff --git a/modules/g2_import/controllers/g2.php b/modules/g2_import/controllers/g2.php
index abbf87a0..c24d52e9 100644
--- a/modules/g2_import/controllers/g2.php
+++ b/modules/g2_import/controllers/g2.php
@@ -46,7 +46,7 @@ class G2_Controller extends Controller {
}
if (!$id) {
- url::redirect("tag_name/$tag_name");
+ url::redirect("tag_name/$tag_name", 301);
}
$tag = ORM::factory("tag")->where("name", "=", $tag_name)->find();
@@ -68,7 +68,7 @@ class G2_Controller extends Controller {
if ($view == "core.DownloadItem") {
$where[] = array("resource_type", "IN", array("file", "resize", "thumbnail", "full"));
} else if ($view) {
- $where[] = array("g2_url", "like", "%g2_view=$view%");
+ $where[] = array("g2_url", "LIKE", "%" . Database::escape_for_like("g2_view=$view") . "%");
} // else: Assuming that the first search hit is sufficiently good.
} else if ($path) {
$where = array(array("g2_url", "IN", array($path, str_replace(" ", "+", $path))));
@@ -99,18 +99,18 @@ class G2_Controller extends Controller {
// Redirect the user to the new url
switch ($resource_type) {
case "thumbnail":
- url::redirect($item->thumb_url(true));
+ url::redirect($item->thumb_url(true), 301);
case "resize":
- url::redirect($item->resize_url(true));
+ url::redirect($item->resize_url(true), 301);
case "file":
case "full":
- url::redirect($item->file_url(true));
+ url::redirect($item->file_url(true), 301);
case "item":
case "album":
- url::redirect($item->abs_url());
+ url::redirect($item->abs_url(), 301);
case "group":
case "user":
diff --git a/modules/g2_import/helpers/g2_import_installer.php b/modules/g2_import/helpers/g2_import_installer.php
index b0c14425..c7569819 100644
--- a/modules/g2_import/helpers/g2_import_installer.php
+++ b/modules/g2_import/helpers/g2_import_installer.php
@@ -31,7 +31,6 @@ class g2_import_installer {
KEY `g2_id` (`g2_id`))
DEFAULT CHARSET=utf8;");
- module::set_version("g2_import", 2);
mkdir(VARPATH . "modules/g2_import");
}
diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php
index e14fe347..0fb033a8 100644
--- a/modules/gallery/controllers/albums.php
+++ b/modules/gallery/controllers/albums.php
@@ -93,10 +93,16 @@ class Albums_Controller extends Items_Controller {
"previous_item" => $previous_item,
"next_item" => $next_item,
"sibling_count" => $item->parent()->viewable()->children_count($where),
+ "siblings_callback" => array("Albums_Controller::get_siblings", array($item)),
"parents" => $item->parents()->as_array(),
"breadcrumbs" => Breadcrumb::array_from_item_parents($item));
}
+ static function get_siblings($item, $limit=null, $offset=null) {
+ // @todo consider creating Item_Model::siblings() if we use this more broadly.
+ return $item->parent()->viewable()->children($limit, $offset);
+ }
+
public function create($parent_id) {
access::verify_csrf();
$album = ORM::factory("item", $parent_id);
diff --git a/modules/gallery/controllers/file_proxy.php b/modules/gallery/controllers/file_proxy.php
index a9e98ce1..7e5d0038 100644
--- a/modules/gallery/controllers/file_proxy.php
+++ b/modules/gallery/controllers/file_proxy.php
@@ -129,6 +129,13 @@ class File_Proxy_Controller extends Controller {
throw $e;
}
+ if (gallery::show_profiler()) {
+ Profiler::enable();
+ $profiler = new Profiler();
+ $profiler->render();
+ exit;
+ }
+
header("Content-Length: " . filesize($file));
header("Pragma:");
diff --git a/modules/gallery/controllers/packager.php b/modules/gallery/controllers/packager.php
index c48965b5..d7e3cf41 100644
--- a/modules/gallery/controllers/packager.php
+++ b/modules/gallery/controllers/packager.php
@@ -88,14 +88,17 @@ class Packager_Controller extends Controller {
$dbconfig = Kohana::config('database.default');
$conn = $dbconfig["connection"];
- $pass = $conn["pass"] ? "-p{$conn['pass']}" : "";
$sql_file = DOCROOT . "installer/install.sql";
if (!is_writable($sql_file)) {
print "$sql_file is not writeable";
return;
}
- $command = "mysqldump --compact --skip-extended-insert --add-drop-table -h{$conn['host']} " .
- "-u{$conn['user']} $pass {$conn['database']} > $sql_file";
+ $command = sprintf(
+ "mysqldump --compact --skip-extended-insert --add-drop-table %s %s %s %s > $sql_file",
+ escapeshellarg("-h{$conn['host']}"),
+ escapeshellarg("-u{$conn['user']}"),
+ $conn['pass'] ? escapeshellarg("-p{$conn['pass']}") : "",
+ escapeshellarg($conn['database']));
exec($command, $output, $status);
if ($status) {
print "<pre>";
diff --git a/modules/gallery/controllers/upgrader.php b/modules/gallery/controllers/upgrader.php
index e60385d9..d3c6e2ec 100644
--- a/modules/gallery/controllers/upgrader.php
+++ b/modules/gallery/controllers/upgrader.php
@@ -107,7 +107,11 @@ class Upgrader_Controller extends Controller {
print "Upgrade complete\n";
}
} else {
- url::redirect("upgrader?failed=" . join(",", $failed));
+ if ($failed) {
+ url::redirect("upgrader?failed=" . join(",", $failed));
+ } else {
+ url::redirect("upgrader");
+ }
}
}
}
diff --git a/modules/gallery/controllers/uploader.php b/modules/gallery/controllers/uploader.php
index b2ec5b55..78437071 100644
--- a/modules/gallery/controllers/uploader.php
+++ b/modules/gallery/controllers/uploader.php
@@ -63,13 +63,9 @@ class Uploader_Controller extends Controller {
$item->parent_id = $album->id;
$item->set_data_file($temp_filename);
- // Remove double extensions from the filename - they'll be disallowed in the model but if
- // we don't do it here then it'll result in a failed upload.
- $item->name = legal_file::smash_extensions($item->name);
-
$path_info = @pathinfo($temp_filename);
if (array_key_exists("extension", $path_info) &&
- in_array(strtolower($path_info["extension"]), legal_file::get_movie_extensions())) {
+ legal_file::get_movie_extensions($path_info["extension"])) {
$item->type = "movie";
$item->save();
log::success("content", t("Added a movie"),
diff --git a/modules/gallery/helpers/data_rest.php b/modules/gallery/helpers/data_rest.php
index dc213510..d4f456d7 100644
--- a/modules/gallery/helpers/data_rest.php
+++ b/modules/gallery/helpers/data_rest.php
@@ -47,7 +47,16 @@ class data_rest_Core {
throw new Kohana_404_Exception();
}
- // We don't have a cache buster in the url, so don't set cache headers here.
+ header("Content-Length: " . filesize($file));
+
+ if (isset($p->m)) {
+ header("Pragma:");
+ // Check that the content hasn't expired or it wasn't changed since cached
+ expires::check(2592000, $item->updated);
+
+ expires::set(2592000, $item->updated); // 30 days
+ }
+
// We don't need to save the session for this request
Session::instance()->abort_save();
@@ -84,6 +93,18 @@ class data_rest_Core {
}
static function url($item, $size) {
- return url::abs_site("rest/data/{$item->id}?size=$size");
+ if ($size == "full") {
+ $file = $item->file_path();
+ } else if ($size == "resize") {
+ $file = $item->resize_path();
+ } else {
+ $file = $item->thumb_path();
+ }
+ if (!file_exists($file)) {
+ throw new Kohana_404_Exception();
+ }
+
+ return url::abs_site("rest/data/{$item->id}?size=$size&m=" . filemtime($file));
}
}
+
diff --git a/modules/gallery/helpers/gallery.php b/modules/gallery/helpers/gallery.php
index 38002b58..f1f7190c 100644
--- a/modules/gallery/helpers/gallery.php
+++ b/modules/gallery/helpers/gallery.php
@@ -214,4 +214,20 @@ class gallery_Core {
}
return null;
}
+
+ /**
+ * Return true if we should show the profiler at the bottom of the page. Note that this
+ * function is called at database setup time so it cannot rely on the database.
+ */
+ static function show_profiler() {
+ return file_exists(VARPATH . "PROFILE");
+ }
+
+ /**
+ * Return true if we should allow Javascript and CSS combining for performance reasons.
+ * Typically we want this, but it's convenient for developers to be able to disable it.
+ */
+ static function allow_css_and_js_combining() {
+ return !file_exists(VARPATH . "DONT_COMBINE");
+ }
} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery_event.php b/modules/gallery/helpers/gallery_event.php
index bd9acc1c..aeb1c7eb 100644
--- a/modules/gallery/helpers/gallery_event.php
+++ b/modules/gallery/helpers/gallery_event.php
@@ -86,17 +86,17 @@ class gallery_event_Core {
static function item_created($item) {
access::add_item($item);
- if ($item->is_photo() || $item->is_movie()) {
- // Build our thumbnail/resizes.
- try {
- graphics::generate($item);
- } catch (Exception $e) {
- log::error("graphics", t("Couldn't create a thumbnail or resize for %item_title",
- array("item_title" => $item->title)),
- html::anchor($item->abs_url(), t("details")));
- Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString());
- }
+ // Build our thumbnail/resizes.
+ try {
+ graphics::generate($item);
+ } catch (Exception $e) {
+ log::error("graphics", t("Couldn't create a thumbnail or resize for %item_title",
+ array("item_title" => $item->title)),
+ html::anchor($item->abs_url(), t("details")));
+ Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString());
+ }
+ if ($item->is_photo() || $item->is_movie()) {
// If the parent has no cover item, make this it.
$parent = $item->parent();
if (access::can("edit", $parent) && $parent->album_cover_item_id == null) {
@@ -141,10 +141,9 @@ class gallery_event_Core {
foreach (ORM::factory("item")
->where("album_cover_item_id", "=", $item->id)
->find_all() as $target) {
- copy($item->thumb_path(), $target->thumb_path());
- $target->thumb_width = $item->thumb_width;
- $target->thumb_height = $item->thumb_height;
+ $target->thumb_dirty = 1;
$target->save();
+ graphics::generate($target);
}
}
@@ -347,9 +346,9 @@ class gallery_event_Core {
if (($item->type == "album" && empty($item->album_cover_item_id)) ||
($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) ||
$parent->album_cover_item_id == $item->id) {
- $disabledState = " ui-state-disabled";
+ $disabledState = "ui-state-disabled";
} else {
- $disabledState = " ";
+ $disabledState = "";
}
if ($item->parent()->id != 1) {
@@ -358,7 +357,7 @@ class gallery_event_Core {
Menu::factory("ajax_link")
->id("make_album_cover")
->label(t("Choose as the album cover"))
- ->css_class("ui-icon-star")
+ ->css_class("ui-icon-star $disabledState")
->ajax_handler("function(data) { window.location.reload() }")
->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf")));
}
@@ -501,16 +500,16 @@ class gallery_event_Core {
if (($item->type == "album" && empty($item->album_cover_item_id)) ||
($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) ||
$parent->album_cover_item_id == $item->id) {
- $disabledState = " ui-state-disabled";
+ $disabledState = "ui-state-disabled";
} else {
- $disabledState = " ";
+ $disabledState = "";
}
if ($item->parent()->id != 1) {
$options_menu
->append(Menu::factory("ajax_link")
->id("make_album_cover")
->label($cover_title)
- ->css_class("ui-icon-star")
+ ->css_class("ui-icon-star $disabledState")
->ajax_handler("function(data) { window.location.reload() }")
->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf")));
}
@@ -519,7 +518,8 @@ class gallery_event_Core {
->id("delete")
->label($delete_title)
->css_class("ui-icon-trash")
- ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&amp;from_id={$theme_item->id}&amp;page_type=$page_type")));
+ ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&amp;" .
+ "from_id={$theme_item->id}&amp;page_type=$page_type")));
}
if ($item->is_album()) {
diff --git a/modules/gallery/helpers/gallery_graphics.php b/modules/gallery/helpers/gallery_graphics.php
index 1f0728c0..eb76353f 100644
--- a/modules/gallery/helpers/gallery_graphics.php
+++ b/modules/gallery/helpers/gallery_graphics.php
@@ -29,12 +29,28 @@ class gallery_graphics_Core {
static function rotate($input_file, $output_file, $options, $item=null) {
graphics::init_toolkit();
- module::event("graphics_rotate", $input_file, $output_file, $options, $item);
+ $temp_file = system::temp_filename("rotate_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_rotate", $input_file, $temp_file, $options, $item);
- Image::factory($input_file)
- ->quality(module::get_var("gallery", "image_quality"))
- ->rotate($options["degrees"])
- ->save($output_file);
+ if (@filesize($temp_file) > 0) {
+ // A graphics_rotate event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
+ } else {
+ // No events made an image - proceed with standard process.
+ if (@filesize($input_file) == 0) {
+ throw new Exception("@todo EMPTY_INPUT_FILE");
+ }
+
+ if (!isset($options["degrees"])) {
+ $options["degrees"] = 0;
+ }
+
+ // Rotate the image. This also implicitly converts its format if needed.
+ Image::factory($input_file)
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->rotate($options["degrees"])
+ ->save($output_file);
+ }
module::event("graphics_rotate_completed", $input_file, $output_file, $options, $item);
}
@@ -51,24 +67,46 @@ class gallery_graphics_Core {
static function resize($input_file, $output_file, $options, $item=null) {
graphics::init_toolkit();
- module::event("graphics_resize", $input_file, $output_file, $options, $item);
-
- if (@filesize($input_file) == 0) {
- throw new Exception("@todo EMPTY_INPUT_FILE");
- }
+ $temp_file = system::temp_filename("resize_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_resize", $input_file, $temp_file, $options, $item);
- $dims = getimagesize($input_file);
- if (max($dims[0], $dims[1]) <= min($options["width"], $options["height"])) {
- // Image would get upscaled; do nothing
- copy($input_file, $output_file);
+ if (@filesize($temp_file) > 0) {
+ // A graphics_resize event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
} else {
- $image = Image::factory($input_file)
- ->resize($options["width"], $options["height"], $options["master"])
- ->quality(module::get_var("gallery", "image_quality"));
- if (graphics::can("sharpen")) {
- $image->sharpen(module::get_var("gallery", "image_sharpen"));
+ // No events made an image - proceed with standard process.
+ if (@filesize($input_file) == 0) {
+ throw new Exception("@todo EMPTY_INPUT_FILE");
+ }
+
+ list ($input_width, $input_height, $input_mime, $input_extension) =
+ photo::get_file_metadata($input_file);
+ if ($input_width && $input_height &&
+ (empty($options["width"]) || empty($options["height"]) || empty($options["master"]) ||
+ (max($input_width, $input_height) <= min($options["width"], $options["height"])))) {
+ // Photo dimensions well-defined, but options not well-defined or would upscale the image.
+ // Do not resize. Check mimes to see if we can copy the file or if we need to convert it.
+ // (checking mimes avoids needlessly converting jpg to jpeg, etc.)
+ $output_mime = legal_file::get_photo_types_by_extension(pathinfo($output_file, PATHINFO_EXTENSION));
+ if ($input_mime && $output_mime && ($input_mime == $output_mime)) {
+ // Mimes well-defined and identical - copy input to output
+ copy($input_file, $output_file);
+ } else {
+ // Mimes not well-defined or not the same - convert input to output
+ $image = Image::factory($input_file)
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->save($output_file);
+ }
+ } else {
+ // Resize the image. This also implicitly converts its format if needed.
+ $image = Image::factory($input_file)
+ ->resize($options["width"], $options["height"], $options["master"])
+ ->quality(module::get_var("gallery", "image_quality"));
+ if (graphics::can("sharpen")) {
+ $image->sharpen(module::get_var("gallery", "image_sharpen"));
+ }
+ $image->save($output_file);
}
- $image->save($output_file);
}
module::event("graphics_resize_completed", $input_file, $output_file, $options, $item);
@@ -94,38 +132,51 @@ class gallery_graphics_Core {
try {
graphics::init_toolkit();
- module::event("graphics_composite", $input_file, $output_file, $options, $item);
-
- list ($width, $height) = getimagesize($input_file);
- list ($w_width, $w_height) = getimagesize($options["file"]);
-
- $pad = isset($options["padding"]) ? $options["padding"] : 10;
- $top = $pad;
- $left = $pad;
- $y_center = max($height / 2 - $w_height / 2, $pad);
- $x_center = max($width / 2 - $w_width / 2, $pad);
- $bottom = max($height - $w_height - $pad, $pad);
- $right = max($width - $w_width - $pad, $pad);
-
- switch ($options["position"]) {
- case "northwest": $x = $left; $y = $top; break;
- case "north": $x = $x_center; $y = $top; break;
- case "northeast": $x = $right; $y = $top; break;
- case "west": $x = $left; $y = $y_center; break;
- case "center": $x = $x_center; $y = $y_center; break;
- case "east": $x = $right; $y = $y_center; break;
- case "southwest": $x = $left; $y = $bottom; break;
- case "south": $x = $x_center; $y = $bottom; break;
- case "southeast": $x = $right; $y = $bottom; break;
- }
+ $temp_file = system::temp_filename("composite_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_composite", $input_file, $temp_file, $options, $item);
- Image::factory($input_file)
- ->composite($options["file"], $x, $y, $options["transparency"])
- ->quality(module::get_var("gallery", "image_quality"))
- ->save($output_file);
+ if (@filesize($temp_file) > 0) {
+ // A graphics_composite event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
+ } else {
+ // No events made an image - proceed with standard process.
+
+ list ($width, $height) = photo::get_file_metadata($input_file);
+ list ($w_width, $w_height) = photo::get_file_metadata($options["file"]);
+
+ $pad = isset($options["padding"]) ? $options["padding"] : 10;
+ $top = $pad;
+ $left = $pad;
+ $y_center = max($height / 2 - $w_height / 2, $pad);
+ $x_center = max($width / 2 - $w_width / 2, $pad);
+ $bottom = max($height - $w_height - $pad, $pad);
+ $right = max($width - $w_width - $pad, $pad);
+
+ switch ($options["position"]) {
+ case "northwest": $x = $left; $y = $top; break;
+ case "north": $x = $x_center; $y = $top; break;
+ case "northeast": $x = $right; $y = $top; break;
+ case "west": $x = $left; $y = $y_center; break;
+ case "center": $x = $x_center; $y = $y_center; break;
+ case "east": $x = $right; $y = $y_center; break;
+ case "southwest": $x = $left; $y = $bottom; break;
+ case "south": $x = $x_center; $y = $bottom; break;
+ case "southeast": $x = $right; $y = $bottom; break;
+ }
+
+ Image::factory($input_file)
+ ->composite($options["file"], $x, $y, $options["transparency"])
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->save($output_file);
+ }
module::event("graphics_composite_completed", $input_file, $output_file, $options, $item);
} catch (ErrorException $e) {
+ // Unlike rotate and resize, composite catches its exceptions here. This is because
+ // composite is typically called for watermarks. If during thumb/resize generation
+ // the watermark fails, we'd still like the image resized, just without its watermark.
+ // If the exception isn't caught here, graphics::generate will replace it with a
+ // placeholder.
Kohana_Log::add("error", $e->getMessage());
}
}
diff --git a/modules/gallery/helpers/gallery_installer.php b/modules/gallery/helpers/gallery_installer.php
index 91c785f6..051a66cf 100644
--- a/modules/gallery/helpers/gallery_installer.php
+++ b/modules/gallery/helpers/gallery_installer.php
@@ -116,7 +116,8 @@ class gallery_installer {
KEY `type` (`type`),
KEY `random` (`rand_key`),
KEY `weight` (`weight` DESC),
- KEY `left_ptr` (`left_ptr`))
+ KEY `left_ptr` (`left_ptr`),
+ KEY `relative_path_cache` (`relative_path_cache`))
DEFAULT CHARSET=utf8;");
$db->query("CREATE TABLE {logs} (
@@ -314,8 +315,7 @@ class gallery_installer {
module::set_var("gallery", "timezone", null);
module::set_var("gallery", "lock_timeout", 1);
module::set_var("gallery", "movie_extract_frame_time", 3);
-
- module::set_version("gallery", 53);
+ module::set_var("gallery", "movie_allow_uploads", "autodetect");
}
static function upgrade($version) {
@@ -718,14 +718,14 @@ class gallery_installer {
if ($version == 50) {
// In v51, we added a lock_timeout variable so that administrators could edit the time out
- // from 1 second to a higher variable if their system runs concurrent parallel uploads for
+ // from 1 second to a higher variable if their system runs concurrent parallel uploads for
// instance.
module::set_var("gallery", "lock_timeout", 1);
module::set_version("gallery", $version = 51);
}
if ($version == 51) {
- // In v52, we added functions to the legal_file helper that map photo and movie file
+ // In v52, we added functions to the legal_file helper that map photo and movie file
// extensions to their mime types (and allow extension of the list by other modules). During
// this process, we correctly mapped m4v files to video/x-m4v, correcting a previous error
// where they were mapped to video/mp4. This corrects the existing items.
@@ -736,13 +736,67 @@ class gallery_installer {
->execute();
module::set_version("gallery", $version = 52);
}
-
+
if ($version == 52) {
// In v53, we added the ability to change the default time used when extracting frames from
// movies. Previously we hard-coded this at 3 seconds, so we use that as the default.
module::set_var("gallery", "movie_extract_frame_time", 3);
module::set_version("gallery", $version = 53);
}
+
+ if ($version == 53) {
+ // In v54, we changed how we check for name and slug conflicts in Item_Model. Previously,
+ // we checked the whole filename. As a result, "foo.jpg" and "foo.png" were not considered
+ // conflicting if their slugs were different (a rare case in practice since server_add and
+ // uploader would give them both the same slug "foo"). Now, we check the filename without its
+ // extension. This upgrade stanza fixes any conflicts where they were previously allowed.
+
+ // This might be slow, but if it times out it can just pick up where it left off.
+
+ // Find and loop through each conflict (e.g. "foo.jpg", "foo.png", and "foo.flv" are one
+ // conflict; "bar.jpg", "bar.png", and "bar.flv" are another)
+ foreach (db::build()
+ ->select_distinct(array("parent_base_name" =>
+ db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))")))
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->where("type", "<>", "album")
+ ->having("C", ">", 1)
+ ->group_by("parent_base_name")
+ ->execute() as $conflict) {
+ list ($parent_id, $base_name) = explode(":", $conflict->parent_base_name, 2);
+ $base_name_escaped = Database::escape_for_like($base_name);
+ // Loop through the items for each conflict
+ foreach (db::build()
+ ->from("items")
+ ->select("id")
+ ->where("type", "<>", "album")
+ ->where("parent_id", "=", $parent_id)
+ ->where("name", "LIKE", "{$base_name_escaped}.%")
+ ->limit(1000000) // required to satisfy SQL syntax (no offset without limit)
+ ->offset(1) // skips the 0th item
+ ->execute() as $row) {
+ set_time_limit(30);
+ $item = ORM::factory("item", $row->id);
+ $item->name = $item->name; // this will force Item_Model to check for conflicts on save
+ $item->save();
+ }
+ }
+ module::set_version("gallery", $version = 54);
+ }
+
+ if ($version == 54) {
+ $db->query("ALTER TABLE {items} ADD KEY `relative_path_cache` (`relative_path_cache`)");
+ module::set_version("gallery", $version = 55);
+ }
+
+ if ($version == 55) {
+ // In v56, we added the ability to change the default behavior regarding movie uploads. It
+ // can be set to "always", "never", or "autodetect" to match the previous behavior where they
+ // are allowed only if FFmpeg is found.
+ module::set_var("gallery", "movie_allow_uploads", "autodetect");
+ module::set_version("gallery", $version = 56);
+ }
}
static function uninstall() {
diff --git a/modules/gallery/helpers/gallery_task.php b/modules/gallery/helpers/gallery_task.php
index 82ca9ffc..856d2639 100644
--- a/modules/gallery/helpers/gallery_task.php
+++ b/modules/gallery/helpers/gallery_task.php
@@ -26,11 +26,13 @@ class gallery_task_Core {
const FIX_STATE_RUN_DUPE_SLUGS = 5;
const FIX_STATE_START_DUPE_NAMES = 6;
const FIX_STATE_RUN_DUPE_NAMES = 7;
- const FIX_STATE_START_REBUILD_ITEM_CACHES = 8;
- const FIX_STATE_RUN_REBUILD_ITEM_CACHES = 9;
- const FIX_STATE_START_MISSING_ACCESS_CACHES = 10;
- const FIX_STATE_RUN_MISSING_ACCESS_CACHES = 11;
- const FIX_STATE_DONE = 12;
+ const FIX_STATE_START_DUPE_BASE_NAMES = 8;
+ const FIX_STATE_RUN_DUPE_BASE_NAMES = 9;
+ const FIX_STATE_START_REBUILD_ITEM_CACHES = 10;
+ const FIX_STATE_RUN_REBUILD_ITEM_CACHES = 11;
+ const FIX_STATE_START_MISSING_ACCESS_CACHES = 12;
+ const FIX_STATE_RUN_MISSING_ACCESS_CACHES = 13;
+ const FIX_STATE_DONE = 14;
static function available_tasks() {
$dirty_count = graphics::find_dirty_images_query()->count_records();
@@ -348,8 +350,9 @@ class gallery_task_Core {
// album audit (permissions and bogus album covers): 1 operation for every album
$total += db::build()->where("type", "=", "album")->count_records("items");
- // one operation for each missing slug, name and access cache
- foreach (array("find_dupe_slugs", "find_dupe_names", "find_missing_access_caches") as $func) {
+ // one operation for each dupe slug, dupe name, dupe base name, and missing access cache
+ foreach (array("find_dupe_slugs", "find_dupe_names", "find_dupe_base_names",
+ "find_missing_access_caches") as $func) {
foreach (self::$func() as $row) {
$total++;
}
@@ -489,11 +492,12 @@ class gallery_task_Core {
$task->set("stack", implode(" ", $stack));
$state = self::FIX_STATE_RUN_DUPE_NAMES;
} else {
- $state = self::FIX_STATE_START_ALBUMS;
+ $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
}
break;
case self::FIX_STATE_RUN_DUPE_NAMES:
+ // NOTE: This does *not* attempt to fix the file system!
$stack = explode(" ", $task->get("stack"));
list ($parent_id, $name) = explode(":", array_pop($stack));
@@ -505,9 +509,16 @@ class gallery_task_Core {
->find_all(1, 1);
if ($conflicts->count() && $conflict = $conflicts->current()) {
$task->log("Fixing conflicting name for item id {$conflict->id}");
+ if (!$conflict->is_album() && preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) {
+ $item_base_name = $matches[1];
+ $item_extension = $matches[2]; // includes a leading dot
+ } else {
+ $item_base_name = $conflict->name;
+ $item_extension = "";
+ }
db::build()
->update("items")
- ->set("name", $name . "-" . (string)rand(1000, 9999))
+ ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension)
->where("id", "=", $conflict->id)
->execute();
@@ -522,6 +533,74 @@ class gallery_task_Core {
$completed++;
if (empty($stack)) {
+ $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
+ }
+ break;
+
+ case self::FIX_STATE_START_DUPE_BASE_NAMES:
+ $stack = array();
+ foreach (self::find_dupe_base_names() as $row) {
+ list ($parent_id, $base_name) = explode(":", $row->parent_base_name, 2);
+ $stack[] = join(":", array($parent_id, $base_name));
+ }
+ if ($stack) {
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_DUPE_BASE_NAMES;
+ } else {
+ $state = self::FIX_STATE_START_ALBUMS;
+ }
+ break;
+
+ case self::FIX_STATE_RUN_DUPE_BASE_NAMES:
+ // NOTE: This *does* attempt to fix the file system! So, it must go *after* run_dupe_names.
+ $stack = explode(" ", $task->get("stack"));
+ list ($parent_id, $base_name) = explode(":", array_pop($stack));
+ $base_name_escaped = Database::escape_for_like($base_name);
+
+ $fixed = 0;
+ // We want to leave the first one alone and update all conflicts to be random values.
+ $conflicts = ORM::factory("item")
+ ->where("parent_id", "=", $parent_id)
+ ->where("name", "LIKE", "{$base_name_escaped}.%")
+ ->where("type", "<>", "album")
+ ->find_all(1, 1);
+ if ($conflicts->count() && $conflict = $conflicts->current()) {
+ $task->log("Fixing conflicting name for item id {$conflict->id}");
+ if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) {
+ $item_base_name = $matches[1]; // unlike $base_name, this always maintains capitalization
+ $item_extension = $matches[2]; // includes a leading dot
+ } else {
+ $item_base_name = $conflict->name;
+ $item_extension = "";
+ }
+ // Unlike conflicts found in run_dupe_names, these items are likely to have an intact
+ // file system. Let's use the item save logic to rebuild the paths and rename the files
+ // if possible.
+ try {
+ $conflict->name = $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension;
+ $conflict->validate();
+ // If we get here, we're safe to proceed with save
+ $conflict->save();
+ } catch (Exception $e) {
+ // Didn't work. Edit database directly without fixing file system.
+ db::build()
+ ->update("items")
+ ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension)
+ ->where("id", "=", $conflict->id)
+ ->execute();
+ }
+
+ // We fixed one conflict, but there might be more so put this parent back on the stack
+ // and try again. We won't consider it completed when we don't fix a conflict. This
+ // guarantees that we won't spend too long fixing one set of conflicts, and that we
+ // won't stop before all are fixed.
+ $stack[] = "$parent_id:$base_name";
+ break;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+
+ if (empty($stack)) {
$state = self::FIX_STATE_START_ALBUMS;
}
break;
@@ -612,7 +691,7 @@ class gallery_task_Core {
break;
case self::FIX_STATE_RUN_MISSING_ACCESS_CACHES:
- $stack = explode(" ", $task->get("stack"));
+ $stack = array_filter(explode(" ", $task->get("stack"))); // filter removes empty/zero ids
if (!empty($stack)) {
$id = array_pop($stack);
$access_cache = ORM::factory("access_cache");
@@ -669,18 +748,32 @@ class gallery_task_Core {
}
static function find_dupe_names() {
+ // looking for photos, movies, and albums
return db::build()
->select_distinct(
array("parent_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(`name`))")))
->select("id")
->select(array("C" => "COUNT(\"*\")"))
->from("items")
- ->where("type", "<>", "album")
->having("C", ">", 1)
->group_by("parent_name")
->execute();
}
+ static function find_dupe_base_names() {
+ // looking for photos or movies, not albums
+ return db::build()
+ ->select_distinct(
+ array("parent_base_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))")))
+ ->select("id")
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->where("type", "<>", "album")
+ ->having("C", ">", 1)
+ ->group_by("parent_base_name")
+ ->execute();
+ }
+
static function find_empty_item_caches($limit) {
return db::build()
->select("items.id")
diff --git a/modules/gallery/helpers/gallery_theme.php b/modules/gallery/helpers/gallery_theme.php
index f94b9ecd..3c6d71e9 100644
--- a/modules/gallery/helpers/gallery_theme.php
+++ b/modules/gallery/helpers/gallery_theme.php
@@ -71,7 +71,7 @@ class gallery_theme_Core {
static function page_bottom($theme) {
$session = Session::instance();
- if ($session->get("profiler", false)) {
+ if (gallery::show_profiler()) {
Profiler::enable();
$profiler = new Profiler();
$profiler->render();
@@ -96,7 +96,7 @@ class gallery_theme_Core {
static function admin_page_bottom($theme) {
$session = Session::instance();
- if ($session->get("profiler", false)) {
+ if (gallery::show_profiler()) {
Profiler::enable();
$profiler = new Profiler();
$profiler->render();
diff --git a/modules/gallery/helpers/graphics.php b/modules/gallery/helpers/graphics.php
index 51437d4b..e66908c4 100644
--- a/modules/gallery/helpers/graphics.php
+++ b/modules/gallery/helpers/graphics.php
@@ -115,36 +115,12 @@ class graphics_Core {
* @param Item_Model $item
*/
static function generate($item) {
- if ($item->is_album()) {
- if (!$cover = $item->album_cover()) {
- // This album has no cover; there's nothing to generate. Because of an old bug, it's
- // possible that there's an album cover item id that points to an invalid item. In that
- // case, just null out the album cover item id. It's not optimal to do that at this low
- // level, but it's not trivial to find these cases quickly in an upgrade script and if we
- // don't do this, the album may be permanently marked as "needs rebuilding"
- //
- // ref: http://sourceforge.net/apps/trac/gallery/ticket/1172
- // http://galleryproject.org/node/96926
- if ($item->album_cover_item_id) {
- $item->album_cover_item_id = null;
- $item->save();
- }
- return;
- }
- $input_file = $cover->file_path();
- $input_item = $cover;
- } else {
- $input_file = $item->file_path();
- $input_item = $item;
- }
-
if ($item->thumb_dirty) {
$ops["thumb"] = $item->thumb_path();
}
- if ($item->resize_dirty && !$item->is_album() && !$item->is_movie()) {
+ if ($item->resize_dirty && $item->is_photo()) {
$ops["resize"] = $item->resize_path();
}
-
if (empty($ops)) {
$item->thumb_dirty = 0;
$item->resize_dirty = 0;
@@ -154,10 +130,11 @@ class graphics_Core {
try {
foreach ($ops as $target => $output_file) {
- if ($input_item->is_movie()) {
- // Convert the movie filename to a JPG first, delete anything that might already be there
- $output_file = legal_file::change_extension($output_file, "jpg");
- @unlink($output_file);
+ $working_file = $item->file_path();
+ // Delete anything that might already be there
+ @unlink($output_file);
+ switch ($item->type) {
+ case "movie":
// Run movie_extract_frame events, which can either:
// - generate an output file, bypassing the ffmpeg-based movie::extract_frame
// - add to the options sent to movie::extract_frame (e.g. change frame extract time,
@@ -165,27 +142,70 @@ class graphics_Core {
// Note that the args are similar to those of the events in gallery_graphics
$movie_options_wrapper = new stdClass();
$movie_options_wrapper->movie_options = array();
- module::event("movie_extract_frame", $input_file, $output_file,
- $movie_options_wrapper, $input_item);
+ module::event("movie_extract_frame", $working_file, $output_file,
+ $movie_options_wrapper, $item);
// If no output_file generated by events, run movie::extract_frame with movie_options
clearstatcache();
if (@filesize($output_file) == 0) {
try {
- movie::extract_frame($input_file, $output_file, $movie_options_wrapper->movie_options);
+ movie::extract_frame($working_file, $output_file, $movie_options_wrapper->movie_options);
+ // If we're here, we know ffmpeg is installed and the movie is valid. Because the
+ // user may not always have had ffmpeg installed, the movie's width, height, and
+ // mime type may need updating. Let's use this opportunity to make sure they're
+ // correct. It's not optimal to do it at this low level, but it's not trivial to find
+ // these cases quickly in an upgrade script.
+ list ($width, $height, $mime_type) = movie::get_file_metadata($working_file);
+ // Only set them if they need updating to avoid marking them as "changed"
+ if (($item->width != $width) || ($item->height != $height) ||
+ ($item->mime_type != $mime_type)) {
+ $item->width = $width;
+ $item->height = $height;
+ $item->mime_type = $mime_type;
+ }
} catch (Exception $e) {
- // Didn't work, likely because of MISSING_FFMPEG - copy missing_movie instead
- copy(MODPATH . "gallery/images/missing_movie.jpg", $output_file);
+ // Didn't work, likely because of MISSING_FFMPEG - use placeholder
+ graphics::_replace_image_with_placeholder($item, $target);
+ break;
}
}
$working_file = $output_file;
- } else {
- $working_file = $input_file;
- }
- foreach (self::_get_rules($target) as $rule) {
- $args = array($working_file, $output_file, unserialize($rule->args), $item);
- call_user_func_array($rule->operation, $args);
- $working_file = $output_file;
+ case "photo":
+ // Run the graphics rules (for both movies and photos)
+ foreach (self::_get_rules($target) as $rule) {
+ $args = array($working_file, $output_file, unserialize($rule->args), $item);
+ call_user_func_array($rule->operation, $args);
+ $working_file = $output_file;
+ }
+ break;
+
+ case "album":
+ if (!$cover = $item->album_cover()) {
+ // This album has no cover; copy its placeholder image. Because of an old bug, it's
+ // possible that there's an album cover item id that points to an invalid item. In that
+ // case, just null out the album cover item id. It's not optimal to do that at this low
+ // level, but it's not trivial to find these cases quickly in an upgrade script and if we
+ // don't do this, the album may be permanently marked as "needs rebuilding"
+ //
+ // ref: http://sourceforge.net/apps/trac/gallery/ticket/1172
+ // http://galleryproject.org/node/96926
+ if ($item->album_cover_item_id) {
+ $item->album_cover_item_id = null;
+ $item->save();
+ }
+ graphics::_replace_image_with_placeholder($item, $target);
+ break;
+ }
+ if ($cover->thumb_dirty) {
+ graphics::generate($cover);
+ }
+ if (!$cover->thumb_dirty) {
+ // Make the album cover from the cover item's thumb. Run gallery_graphics::resize with
+ // null options and it will figure out if this is a direct copy or conversion to jpg.
+ $working_file = $cover->thumb_path();
+ gallery_graphics::resize($working_file, $output_file, null, $item);
+ }
+ break;
}
}
@@ -193,33 +213,93 @@ class graphics_Core {
if (file_exists($item->thumb_path())) {
$item->thumb_dirty = 0;
} else {
- copy(MODPATH . "gallery/images/missing_photo.png", $item->thumb_path());
+ Kohana_Log::add("error", "Failed to rebuild thumb image: $item->title");
+ graphics::_replace_image_with_placeholder($item, "thumb");
}
- $dims = getimagesize($item->thumb_path());
- $item->thumb_width = $dims[0];
- $item->thumb_height = $dims[1];
}
if (!empty($ops["resize"])) {
if (file_exists($item->resize_path())) {
$item->resize_dirty = 0;
} else {
- copy(MODPATH . "gallery/images/missing_photo.png", $item->resize_path());
+ Kohana_Log::add("error", "Failed to rebuild resize image: $item->title");
+ graphics::_replace_image_with_placeholder($item, "resize");
}
- $dims = getimagesize($item->resize_path());
- $item->resize_width = $dims[0];
- $item->resize_height = $dims[1];
}
+ graphics::_update_item_dimensions($item);
$item->save();
} catch (Exception $e) {
- // Something went wrong rebuilding the image. Leave it dirty and move on.
- // @todo we should handle this better.
- Kohana_Log::add("error", "Caught exception rebuilding image: {$item->title}\n" .
+ // Something went wrong rebuilding the image. Replace with the placeholder images,
+ // leave it dirty and move on.
+ Kohana_Log::add("error", "Caught exception rebuilding images: {$item->title}\n" .
$e->getMessage() . "\n" . $e->getTraceAsString());
+ if ($item->is_photo()) {
+ graphics::_replace_image_with_placeholder($item, "resize");
+ }
+ graphics::_replace_image_with_placeholder($item, "thumb");
+ try {
+ graphics::_update_item_dimensions($item);
+ } catch (Exception $e) {
+ // Looks like get_file_metadata couldn't identify our placeholders. We should never get
+ // here, but in the odd case we do, we need to do something. Let's put in hardcoded values.
+ if ($item->is_photo()) {
+ list ($item->resize_width, $item->resize_height) = array(200, 200);
+ }
+ list ($item->thumb_width, $item->thumb_height) = array(200, 200);
+ }
+ $item->save();
throw $e;
}
}
+ private static function _update_item_dimensions($item) {
+ if ($item->is_photo()) {
+ list ($item->resize_width, $item->resize_height) =
+ photo::get_file_metadata($item->resize_path());
+ }
+ list ($item->thumb_width, $item->thumb_height) =
+ photo::get_file_metadata($item->thumb_path());
+ }
+
+ private static function _replace_image_with_placeholder($item, $target) {
+ if ($item->is_album() && !$item->album_cover_item_id) {
+ $input_path = MODPATH . "gallery/images/missing_album_cover.jpg";
+ } else if ($item->is_movie() || ($item->is_album() && $item->album_cover()->is_movie())) {
+ $input_path = MODPATH . "gallery/images/missing_movie.jpg";
+ } else {
+ $input_path = MODPATH . "gallery/images/missing_photo.jpg";
+ }
+
+ if ($target == "thumb") {
+ $output_path = $item->thumb_path();
+ $size = module::get_var("gallery", "thumb_size", 200);
+ } else {
+ $output_path = $item->resize_path();
+ $size = module::get_var("gallery", "resize_size", 640);
+ }
+ $options = array("width" => $size, "height" => $size, "master" => Image::AUTO);
+
+ try {
+ // Copy/convert/resize placeholder as needed.
+ gallery_graphics::resize($input_path, $output_path, $options, null);
+ } catch (Exception $e) {
+ // Copy/convert/resize didn't work. Add to the log and copy the jpg version (which could have
+ // a non-jpg extension). This is less than ideal, but it's better than putting nothing
+ // there and causing theme views to act strangely because a file is missing.
+ // @todo we should handle this better.
+ Kohana_Log::add("error", "Caught exception converting placeholder for missing image: " .
+ $item->title . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
+ copy($input_path, $output_path);
+ }
+
+ if (!file_exists($output_path)) {
+ // Copy/convert/resize didn't throw an exception, but still didn't work - do the same as above.
+ // @todo we should handle this better.
+ Kohana_Log::add("error", "Failed to convert placeholder for missing image: $item->title");
+ copy($input_path, $output_path);
+ }
+ }
+
private static function _get_rules($target) {
if (empty(self::$_rules_cache[$target])) {
$rules = array();
@@ -256,12 +336,19 @@ class graphics_Core {
}
/**
- * Mark thumbnails and resizes as dirty. They will have to be rebuilt.
+ * Mark thumbnails and resizes as dirty. They will have to be rebuilt. Optionally, only those of
+ * a specified type and/or mime type can be marked (e.g. $type="movie" to rebuild movies only).
*/
- static function mark_dirty($thumbs, $resizes) {
+ static function mark_dirty($thumbs, $resizes, $type=null, $mime_type=null) {
if ($thumbs || $resizes) {
$db = db::build()
->update("items");
+ if ($type) {
+ $db->where("type", "=", $type);
+ }
+ if ($mime_type) {
+ $db->where("mime_type", "=", $mime_type);
+ }
if ($thumbs) {
$db->set("thumb_dirty", 1);
}
diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php
index 20a865d1..9882a9c5 100644
--- a/modules/gallery/helpers/item.php
+++ b/modules/gallery/helpers/item.php
@@ -39,55 +39,28 @@ class item_Core {
}
}
- $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;
+ $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 "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;
- }
- }
+ 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;
- } 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;
- }
+ 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;
}
}
@@ -103,34 +76,42 @@ class item_Core {
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;
- 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();
+ 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);
- @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);
}
@@ -437,4 +418,16 @@ class item_Core {
}
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
diff --git a/modules/gallery/helpers/item_rest.php b/modules/gallery/helpers/item_rest.php
index 10799567..efeba2ef 100644
--- a/modules/gallery/helpers/item_rest.php
+++ b/modules/gallery/helpers/item_rest.php
@@ -64,7 +64,7 @@ class item_rest_Core {
}
if (isset($p->name)) {
- $orm->where("name", "LIKE", "%{$p->name}%");
+ $orm->where("name", "LIKE", "%" . Database::escape_for_like($p->name) . "%");
}
if (isset($p->type)) {
diff --git a/modules/gallery/helpers/legal_file.php b/modules/gallery/helpers/legal_file.php
index 5768cf14..eb9c25de 100644
--- a/modules/gallery/helpers/legal_file.php
+++ b/modules/gallery/helpers/legal_file.php
@@ -18,6 +18,15 @@
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
class legal_file_Core {
+ private static $photo_types_by_extension;
+ private static $movie_types_by_extension;
+ private static $photo_extensions;
+ private static $movie_extensions;
+ private static $photo_types;
+ private static $movie_types;
+ private static $blacklist = array("php", "php3", "php4", "php5", "phtml", "phtm", "shtml", "shtm",
+ "pl", "cgi", "asp", "sh", "py", "c", "js");
+
/**
* Create a default list of allowed photo MIME types paired with their extensions and then let
* modules modify it. This is an ordered map, mapping extensions to their MIME types.
@@ -26,21 +35,27 @@ class legal_file_Core {
* @param string $extension (opt.) - return MIME of extension; if not given, return complete array
*/
static function get_photo_types_by_extension($extension=null) {
- $types_by_extension_wrapper = new stdClass();
- $types_by_extension_wrapper->types_by_extension = array(
- "jpg" => "image/jpeg", "jpeg" => "image/jpeg", "gif" => "image/gif", "png" => "image/png");
- module::event("photo_types_by_extension", $types_by_extension_wrapper);
+ if (empty(self::$photo_types_by_extension)) {
+ $types_by_extension_wrapper = new stdClass();
+ $types_by_extension_wrapper->types_by_extension = array(
+ "jpg" => "image/jpeg", "jpeg" => "image/jpeg", "gif" => "image/gif", "png" => "image/png");
+ module::event("photo_types_by_extension", $types_by_extension_wrapper);
+ foreach (self::$blacklist as $key) {
+ unset($types_by_extension_wrapper->types_by_extension[$key]);
+ }
+ self::$photo_types_by_extension = $types_by_extension_wrapper->types_by_extension;
+ }
if ($extension) {
// return matching MIME type
$extension = strtolower($extension);
- if (isset($types_by_extension_wrapper->types_by_extension[$extension])) {
- return $types_by_extension_wrapper->types_by_extension[$extension];
+ if (isset(self::$photo_types_by_extension[$extension])) {
+ return self::$photo_types_by_extension[$extension];
} else {
return null;
}
} else {
// return complete array
- return $types_by_extension_wrapper->types_by_extension;
+ return self::$photo_types_by_extension;
}
}
@@ -52,53 +67,114 @@ class legal_file_Core {
* @param string $extension (opt.) - return MIME of extension; if not given, return complete array
*/
static function get_movie_types_by_extension($extension=null) {
- $types_by_extension_wrapper = new stdClass();
- $types_by_extension_wrapper->types_by_extension = array(
- "flv" => "video/x-flv", "mp4" => "video/mp4", "m4v" => "video/x-m4v");
- module::event("movie_types_by_extension", $types_by_extension_wrapper);
+ if (empty(self::$movie_types_by_extension)) {
+ $types_by_extension_wrapper = new stdClass();
+ $types_by_extension_wrapper->types_by_extension = array(
+ "flv" => "video/x-flv", "mp4" => "video/mp4", "m4v" => "video/x-m4v");
+ module::event("movie_types_by_extension", $types_by_extension_wrapper);
+ foreach (self::$blacklist as $key) {
+ unset($types_by_extension_wrapper->types_by_extension[$key]);
+ }
+ self::$movie_types_by_extension = $types_by_extension_wrapper->types_by_extension;
+ }
if ($extension) {
// return matching MIME type
$extension = strtolower($extension);
- if (isset($types_by_extension_wrapper->types_by_extension[$extension])) {
- return $types_by_extension_wrapper->types_by_extension[$extension];
+ if (isset(self::$movie_types_by_extension[$extension])) {
+ return self::$movie_types_by_extension[$extension];
} else {
return null;
}
} else {
// return complete array
- return $types_by_extension_wrapper->types_by_extension;
+ return self::$movie_types_by_extension;
+ }
+ }
+
+ /**
+ * Create a merged list of all allowed photo and movie MIME types paired with their extensions.
+ *
+ * @param string $extension (opt.) - return MIME of extension; if not given, return complete array
+ */
+ static function get_types_by_extension($extension=null) {
+ $types_by_extension = legal_file::get_photo_types_by_extension();
+ if (movie::allow_uploads()) {
+ $types_by_extension = array_merge($types_by_extension,
+ legal_file::get_movie_types_by_extension());
+ }
+ if ($extension) {
+ // return matching MIME type
+ $extension = strtolower($extension);
+ if (isset($types_by_extension[$extension])) {
+ return $types_by_extension[$extension];
+ } else {
+ return null;
+ }
+ } else {
+ // return complete array
+ return $types_by_extension;
}
}
/**
* Create a default list of allowed photo extensions and then let modules modify it.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
*/
- static function get_photo_extensions() {
- $extensions_wrapper = new stdClass();
- $extensions_wrapper->extensions = array_keys(legal_file::get_photo_types_by_extension());
- module::event("legal_photo_extensions", $extensions_wrapper);
- return $extensions_wrapper->extensions;
+ static function get_photo_extensions($extension=null) {
+ if (empty(self::$photo_extensions)) {
+ $extensions_wrapper = new stdClass();
+ $extensions_wrapper->extensions = array_keys(legal_file::get_photo_types_by_extension());
+ module::event("legal_photo_extensions", $extensions_wrapper);
+ self::$photo_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist);
+ }
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), self::$photo_extensions);
+ } else {
+ // return complete array
+ return self::$photo_extensions;
+ }
}
/**
* Create a default list of allowed movie extensions and then let modules modify it.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
*/
- static function get_movie_extensions() {
- $extensions_wrapper = new stdClass();
- $extensions_wrapper->extensions = array_keys(legal_file::get_movie_types_by_extension());
- module::event("legal_movie_extensions", $extensions_wrapper);
- return $extensions_wrapper->extensions;
+ static function get_movie_extensions($extension=null) {
+ if (empty(self::$movie_extensions)) {
+ $extensions_wrapper = new stdClass();
+ $extensions_wrapper->extensions = array_keys(legal_file::get_movie_types_by_extension());
+ module::event("legal_movie_extensions", $extensions_wrapper);
+ self::$movie_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist);
+ }
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), self::$movie_extensions);
+ } else {
+ // return complete array
+ return self::$movie_extensions;
+ }
}
/**
* Create a merged list of all allowed photo and movie extensions.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
*/
- static function get_extensions() {
+ static function get_extensions($extension=null) {
$extensions = legal_file::get_photo_extensions();
- if (movie::find_ffmpeg()) {
+ if (movie::allow_uploads()) {
$extensions = array_merge($extensions, legal_file::get_movie_extensions());
}
- return $extensions;
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), $extensions);
+ } else {
+ // return complete array
+ return $extensions;
+ }
}
/**
@@ -119,10 +195,14 @@ class legal_file_Core {
* (e.g. flv maps to video/x-flv by default, but video/flv is still legal).
*/
static function get_photo_types() {
- $types_wrapper = new stdClass();
- $types_wrapper->types = array_values(legal_file::get_photo_types_by_extension());
- module::event("legal_photo_types", $types_wrapper);
- return $types_wrapper->types;
+ if (empty(self::$photo_types)) {
+ $types_wrapper = new stdClass();
+ // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg).
+ $types_wrapper->types = array_unique(array_values(legal_file::get_photo_types_by_extension()));
+ module::event("legal_photo_types", $types_wrapper);
+ self::$photo_types = $types_wrapper->types;
+ }
+ return self::$photo_types;
}
/**
@@ -131,11 +211,15 @@ class legal_file_Core {
* (e.g. flv maps to video/x-flv by default, but video/flv is still legal).
*/
static function get_movie_types() {
- $types_wrapper = new stdClass();
- $types_wrapper->types = array_values(legal_file::get_movie_types_by_extension());
- $types_wrapper->types[] = "video/flv";
- module::event("legal_movie_types", $types_wrapper);
- return $types_wrapper->types;
+ if (empty(self::$movie_types)) {
+ $types_wrapper = new stdClass();
+ // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg).
+ $types_wrapper->types = array_unique(array_values(legal_file::get_movie_types_by_extension()));
+ $types_wrapper->types[] = "video/flv";
+ module::event("legal_movie_types", $types_wrapper);
+ self::$movie_types = $types_wrapper->types;
+ }
+ return self::$movie_types;
}
/**
@@ -151,6 +235,10 @@ class legal_file_Core {
* Reduce the given file to having a single extension.
*/
static function smash_extensions($filename) {
+ if (!$filename) {
+ // It's harmless, so return it before it causes issues with pathinfo.
+ return $filename;
+ }
$parts = pathinfo($filename);
$result = "";
if ($parts["dirname"] != ".") {
@@ -159,7 +247,64 @@ class legal_file_Core {
$parts["filename"] = str_replace(".", "_", $parts["filename"]);
$parts["filename"] = preg_replace("/[_]+/", "_", $parts["filename"]);
$parts["filename"] = trim($parts["filename"], "_");
- $result .= "{$parts['filename']}.{$parts['extension']}";
+ $result .= isset($parts["extension"]) ? "{$parts['filename']}.{$parts['extension']}" : $parts["filename"];
return $result;
}
+
+ /**
+ * Sanitize a filename for a given type (given as "photo" or "movie") and a target file format
+ * (given as an extension). This returns a completely legal and valid filename,
+ * or throws an exception if the type or extension given is invalid or illegal. It tries to
+ * maintain the filename's original extension even if it's not identical to the given extension
+ * (e.g. don't change "JPG" or "jpeg" to "jpg").
+ *
+ * Note: it is not okay if the extension given is legal but does not match the type (e.g. if
+ * extension is "mp4" and type is "photo", it will throw an exception)
+ *
+ * @param string $filename (with no directory)
+ * @param string $extension (can be uppercase or lowercase)
+ * @param string $type (as "photo" or "movie")
+ * @return string sanitized filename (or null if bad extension argument)
+ */
+ static function sanitize_filename($filename, $extension, $type) {
+ // Check if the type is valid - if so, get the mime types of the
+ // original and target extensions; if not, throw an exception.
+ $original_extension = pathinfo($filename, PATHINFO_EXTENSION);
+ switch ($type) {
+ case "photo":
+ $mime_type = legal_file::get_photo_types_by_extension($extension);
+ $original_mime_type = legal_file::get_photo_types_by_extension($original_extension);
+ break;
+ case "movie":
+ $mime_type = legal_file::get_movie_types_by_extension($extension);
+ $original_mime_type = legal_file::get_movie_types_by_extension($original_extension);
+ break;
+ default:
+ throw new Exception("@todo INVALID_TYPE");
+ }
+
+ // Check if the target extension is blank or invalid - if so, throw an exception.
+ if (!$extension || !$mime_type) {
+ throw new Exception("@todo ILLEGAL_EXTENSION");
+ }
+
+ // Check if the mime types of the original and target extensions match - if not, fix it.
+ if (!$original_extension || ($mime_type != $original_mime_type)) {
+ $filename = legal_file::change_extension($filename, $extension);
+ }
+
+ // It should be a filename without a directory - remove all slashes (and backslashes).
+ $filename = str_replace("/", "_", $filename);
+ $filename = str_replace("\\", "_", $filename);
+
+ // Remove extra dots from the filename. This will also remove extraneous underscores.
+ $filename = legal_file::smash_extensions($filename);
+
+ // It's possible that the filename has no base (e.g. ".jpg") - if so, give it a generic one.
+ if (empty($filename) || (substr($filename, 0, 1) == ".")) {
+ $filename = $type . $filename; // e.g. "photo.jpg" or "movie.mp4"
+ }
+
+ return $filename;
+ }
}
diff --git a/modules/gallery/helpers/module.php b/modules/gallery/helpers/module.php
index f4ab5571..df258e87 100644
--- a/modules/gallery/helpers/module.php
+++ b/modules/gallery/helpers/module.php
@@ -175,9 +175,8 @@ class module_Core {
$installer_class = "{$module_name}_installer";
if (method_exists($installer_class, "install")) {
call_user_func_array(array($installer_class, "install"), array());
- } else {
- module::set_version($module_name, module::available()->$module_name->code_version);
}
+ module::set_version($module_name, module::available()->$module_name->code_version);
// Set the weight of the new module, which controls the order in which the modules are
// loaded. By default, new modules are installed at the end of the priority list. Since the
diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php
index fb3fab80..eda478c7 100644
--- a/modules/gallery/helpers/movie.php
+++ b/modules/gallery/helpers/movie.php
@@ -24,6 +24,8 @@
* Note: by design, this class does not do any permission checking.
*/
class movie_Core {
+ private static $allow_uploads;
+
static function get_edit_form($movie) {
$form = new Forge("movies/update/$movie->id", "", "post", array("id" => "g-edit-movie-form"));
$form->hidden("from_id")->value($movie->id);
@@ -82,7 +84,7 @@ class movie_Core {
// extract frame at start_time, unless movie is too short
$start_time_arg = ($duration >= $start_time + 0.1) ?
"-ss " . movie::seconds_to_hhmmssdd($start_time) : "";
-
+
$input_args = isset($movie_options["input_args"]) ? $movie_options["input_args"] : "";
$output_args = isset($movie_options["output_args"]) ? $movie_options["output_args"] : "";
@@ -110,6 +112,29 @@ class movie_Core {
}
/**
+ * Return true if movie uploads are allowed, false if not. This is based on the
+ * "movie_allow_uploads" Gallery variable as well as whether or not ffmpeg is found.
+ */
+ static function allow_uploads() {
+ if (empty(self::$allow_uploads)) {
+ // Refresh ffmpeg settings
+ $ffmpeg = movie::find_ffmpeg();
+ switch (module::get_var("gallery", "movie_allow_uploads", "autodetect")) {
+ case "always":
+ self::$allow_uploads = true;
+ break;
+ case "never":
+ self::$allow_uploads = false;
+ break;
+ default:
+ self::$allow_uploads = !empty($ffmpeg);
+ break;
+ }
+ }
+ return self::$allow_uploads;
+ }
+
+ /**
* Return the path to the ffmpeg binary if one exists and is executable, or null.
*/
static function find_ffmpeg() {
@@ -123,48 +148,94 @@ class movie_Core {
/**
* Return the width, height, mime_type, extension and duration of the given movie file.
+ * Metadata is first generated using ffmpeg (or set to defaults if it fails),
+ * then can be modified by other modules using movie_get_file_metadata events.
+ *
+ * This function and its use cases are symmetric to those of photo::get_file_metadata.
+ *
+ * @param string $file_path
+ * @return array array($width, $height, $mime_type, $extension, $duration)
+ *
+ * Use cases in detail:
+ * Input is standard movie type (flv/mp4/m4v)
+ * -> return metadata from ffmpeg
+ * Input is *not* standard movie type that is supported by ffmpeg (e.g. avi, mts...)
+ * -> return metadata from ffmpeg
+ * Input is *not* standard movie type that is *not* supported by ffmpeg but is legal
+ * -> return zero width, height, and duration; mime type and extension according to legal_file
+ * Input is *not* standard movie type that is *not* supported by ffmpeg and is *not* legal
+ * -> return zero width, height, and duration; null mime type and extension
+ * Input is not readable or does not exist
+ * -> throw exception
+ * Note: movie_get_file_metadata events can change any of the above cases (except the last one).
*/
static function get_file_metadata($file_path) {
- $ffmpeg = movie::find_ffmpeg();
- if (empty($ffmpeg)) {
- throw new Exception("@todo MISSING_FFMPEG");
+ if (!is_readable($file_path)) {
+ throw new Exception("@todo UNREADABLE_FILE");
}
- $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($file_path) . " 2>&1";
- $result = `$cmd`;
- if (preg_match("/Stream.*?Video:.*?, (\d+)x(\d+)/", $result, $matches_res)) {
- if (preg_match("/Stream.*?Video:.*? \[.*?DAR (\d+):(\d+).*?\]/", $result, $matches_dar) &&
- $matches_dar[1] >= 1 && $matches_dar[2] >= 1) {
- // DAR is defined - determine width based on height and DAR
- // (should always be int, but adding round to be sure)
- $matches_res[1] = round($matches_res[2] * $matches_dar[1] / $matches_dar[2]);
+ $metadata = new stdClass();
+ $ffmpeg = movie::find_ffmpeg();
+ if (!empty($ffmpeg)) {
+ // ffmpeg found - use it to get width, height, and duration.
+ $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($file_path) . " 2>&1";
+ $result = `$cmd`;
+ if (preg_match("/Stream.*?Video:.*?, (\d+)x(\d+)/", $result, $matches_res)) {
+ if (preg_match("/Stream.*?Video:.*? \[.*?DAR (\d+):(\d+).*?\]/", $result, $matches_dar) &&
+ $matches_dar[1] >= 1 && $matches_dar[2] >= 1) {
+ // DAR is defined - determine width based on height and DAR
+ // (should always be int, but adding round to be sure)
+ $matches_res[1] = round($matches_res[2] * $matches_dar[1] / $matches_dar[2]);
+ }
+ list ($metadata->width, $metadata->height) = array($matches_res[1], $matches_res[2]);
+ } else {
+ list ($metadata->width, $metadata->height) = array(0, 0);
+ }
+
+ if (preg_match("/Duration: (\d+:\d+:\d+\.\d+)/", $result, $matches)) {
+ $metadata->duration = movie::hhmmssdd_to_seconds($matches[1]);
+ } else if (preg_match("/duration.*?:.*?(\d+)/", $result, $matches)) {
+ $metadata->duration = $matches[1];
+ } else {
+ $metadata->duration = 0;
}
- list ($width, $height) = array($matches_res[1], $matches_res[2]);
} else {
- list ($width, $height) = array(0, 0);
+ // ffmpeg not found - set width, height, and duration to zero.
+ $metadata->width = 0;
+ $metadata->height = 0;
+ $metadata->duration = 0;
}
- $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
- $extension = $extension ? $extension : "flv"; // No extension? Assume FLV.
- $mime_type = legal_file::get_movie_types_by_extension($extension);
- $mime_type = $mime_type ? $mime_type : "video/x-flv"; // No MIME found? Default to video/x-flv.
-
- if (preg_match("/Duration: (\d+:\d+:\d+\.\d+)/", $result, $matches)) {
- $duration = movie::hhmmssdd_to_seconds($matches[1]);
- } else if (preg_match("/duration.*?:.*?(\d+)/", $result, $matches)) {
- $duration = $matches[1];
+ $extension = pathinfo($file_path, PATHINFO_EXTENSION);
+ if (!$extension ||
+ (!$metadata->mime_type = legal_file::get_movie_types_by_extension($extension))) {
+ // Extension is empty or illegal.
+ $metadata->extension = null;
+ $metadata->mime_type = null;
} else {
- $duration = 0;
+ // Extension is legal (and mime is already set above).
+ $metadata->extension = strtolower($extension);
}
- return array($width, $height, $mime_type, $extension, $duration);
+ // Run movie_get_file_metadata events which can modify the class.
+ module::event("movie_get_file_metadata", $file_path, $metadata);
+
+ // If the post-events results are invalid, throw an exception. Note that, unlike photos, having
+ // zero width and height isn't considered invalid (as is the case when FFmpeg isn't installed).
+ if (!$metadata->mime_type || !$metadata->extension ||
+ ($metadata->mime_type != legal_file::get_movie_types_by_extension($metadata->extension))) {
+ throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE");
+ }
+
+ return array($metadata->width, $metadata->height, $metadata->mime_type,
+ $metadata->extension, $metadata->duration);
}
/**
* Return the time/duration formatted in hh:mm:ss.dd from a number of seconds.
* Useful for inputs to ffmpeg.
*
- * Note that this is similar to date("H:i:s", mktime(0,0,$seconds,0,0,0,0)), but unlike this
+ * Note that this is similar to date("H:i:s", mktime(0,0,$seconds,0,0,0,0)), but unlike this
* approach avoids potential issues with time zone and DST mismatch and/or using deprecated
* features (the last argument of mkdate above, which disables DST, is deprecated as of PHP 5.3).
*/
@@ -172,7 +243,7 @@ class movie_Core {
return sprintf("%02d:%02d:%05.2f", floor($seconds / 3600), floor(($seconds % 3600) / 60),
floor(100 * $seconds % 6000) / 100);
}
-
+
/**
* Return the number of seconds from a time/duration formatted in hh:mm:ss.dd.
* Useful for outputs from ffmpeg.
diff --git a/modules/gallery/helpers/photo.php b/modules/gallery/helpers/photo.php
index 855cd0ae..2d32f0d3 100644
--- a/modules/gallery/helpers/photo.php
+++ b/modules/gallery/helpers/photo.php
@@ -80,20 +80,68 @@ class photo_Core {
/**
* Return the width, height, mime_type and extension of the given image file.
+ * Metadata is first generated using getimagesize (or the legal_file mapping if it fails),
+ * then can be modified by other modules using photo_get_file_metadata events.
+ *
+ * This function and its use cases are symmetric to those of photo::get_file_metadata.
+ *
+ * @param string $file_path
+ * @return array array($width, $height, $mime_type, $extension)
+ *
+ * Use cases in detail:
+ * Input is standard photo type (jpg/png/gif)
+ * -> return metadata from getimagesize()
+ * Input is *not* standard photo type that is supported by getimagesize (e.g. tif, bmp...)
+ * -> return metadata from getimagesize()
+ * Input is *not* standard photo type that is *not* supported by getimagesize but is legal
+ * -> return zero width and height, mime type and extension according to legal_file
+ * Input is *not* standard photo type that is *not* supported by getimagesize and is *not* legal
+ * -> return zero width and height, null mime type and extension
+ * Input is not readable or does not exist
+ * -> throw exception
+ * Note: photo_get_file_metadata events can change any of the above cases (except the last one).
*/
static function get_file_metadata($file_path) {
- $image_info = getimagesize($file_path);
- if ($image_info) {
- $width = $image_info[0];
- $height = $image_info[1];
- $mime_type = $image_info["mime"];
- $extension = image_type_to_extension($image_info[2], false);
- return array($width, $height, $mime_type, $extension);
+ if (!is_readable($file_path)) {
+ throw new Exception("@todo UNREADABLE_FILE");
+ }
+
+ $metadata = new stdClass();
+ if ($image_info = getimagesize($file_path)) {
+ // getimagesize worked - use its results.
+ $metadata->width = $image_info[0];
+ $metadata->height = $image_info[1];
+ $metadata->mime_type = $image_info["mime"];
+ $metadata->extension = image_type_to_extension($image_info[2], false);
+ // We prefer jpg instead of jpeg (which is returned by image_type_to_extension).
+ if ($metadata->extension == "jpeg") {
+ $metadata->extension = "jpg";
+ }
} else {
- // getimagesize failed - use legal_file mapping instead.
- $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
- $mime_type = legal_file::get_photo_types_by_extension($extension);
- return array(0, 0, $mime_type, $extension);
+ // getimagesize failed - try to use legal_file mapping instead.
+ $extension = pathinfo($file_path, PATHINFO_EXTENSION);
+ if (!$extension ||
+ (!$metadata->mime_type = legal_file::get_photo_types_by_extension($extension))) {
+ // Extension is empty or illegal.
+ $metadata->extension = null;
+ $metadata->mime_type = null;
+ } else {
+ // Extension is legal (and mime is already set above).
+ $metadata->extension = strtolower($extension);
+ }
+ $metadata->width = 0;
+ $metadata->height = 0;
}
+
+ // Run photo_get_file_metadata events which can modify the class.
+ module::event("photo_get_file_metadata", $file_path, $metadata);
+
+ // If the post-events results are invalid, throw an exception.
+ if (!$metadata->width || !$metadata->height || !$metadata->mime_type || !$metadata->extension ||
+ ($metadata->mime_type != legal_file::get_photo_types_by_extension($metadata->extension))) {
+ throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE");
+ }
+
+ return array($metadata->width, $metadata->height, $metadata->mime_type, $metadata->extension);
}
}
diff --git a/modules/gallery/helpers/system.php b/modules/gallery/helpers/system.php
index fa834824..e1398103 100644
--- a/modules/gallery/helpers/system.php
+++ b/modules/gallery/helpers/system.php
@@ -20,22 +20,42 @@
class system_Core {
/**
* Return the path to an executable version of the named binary, or null.
- * Traverse the PATH environment variable looking for the given file. If
- * the $priority_path variable is set, check that path first.
+ * The paths are traversed in the following order:
+ * 1. $priority_path (if specified)
+ * 2. Gallery's own bin directory (DOCROOT . "bin")
+ * 3. PATH environment variable
+ * 4. extra_binary_paths Gallery variable (if specified)
+ * In addition, if the file is found inside Gallery's bin directory but
+ * it's not executable, we try to change its permissions to 0755.
+ *
+ * @param string $binary
+ * @param string $priority_path (optional)
+ * @return string path to binary if found; null if not found
*/
static function find_binary($binary, $priority_path=null) {
- $paths = array_merge(
- explode(":", getenv("PATH")),
- explode(":", module::get_var("gallery", "extra_binary_paths")));
+ $bin_path = DOCROOT . "bin";
+
if ($priority_path) {
- array_unshift($paths, $priority_path);
+ $paths = array($priority_path, $bin_path);
+ } else {
+ $paths = array($bin_path);
}
+ $paths = array_merge($paths,
+ explode(":", getenv("PATH")),
+ explode(":", module::get_var("gallery", "extra_binary_paths")));
foreach ($paths as $path) {
$candidate = "$path/$binary";
// @suppress errors below to avoid open_basedir issues
- if (@file_exists($candidate) && @is_executable($candidate)) {
- return $candidate;
+ if (@file_exists($candidate)) {
+ if (!@is_executable($candidate) &&
+ (substr_compare($bin_path, $candidate, 0, strlen($bin_path)) == 0)) {
+ // Binary isn't executable but is in Gallery's bin directory - try fixing permissions.
+ @chmod($candidate, 0755);
+ }
+ if (@is_executable($candidate)) {
+ return $candidate;
+ }
}
}
return null;
diff --git a/modules/gallery/images/missing_album_cover.jpg b/modules/gallery/images/missing_album_cover.jpg
new file mode 100644
index 00000000..bdddeec5
--- /dev/null
+++ b/modules/gallery/images/missing_album_cover.jpg
Binary files differ
diff --git a/modules/gallery/images/missing_photo.jpg b/modules/gallery/images/missing_photo.jpg
new file mode 100644
index 00000000..a9d176d8
--- /dev/null
+++ b/modules/gallery/images/missing_photo.jpg
Binary files differ
diff --git a/modules/gallery/images/missing_photo.png b/modules/gallery/images/missing_photo.png
deleted file mode 100644
index 67786275..00000000
--- a/modules/gallery/images/missing_photo.png
+++ /dev/null
Binary files differ
diff --git a/modules/gallery/js/l10n_client.js b/modules/gallery/js/l10n_client.js
index a1b970e7..6d919c29 100644
--- a/modules/gallery/js/l10n_client.js
+++ b/modules/gallery/js/l10n_client.js
@@ -140,7 +140,11 @@ jQuery.extend(Gallery, {
} else {
if(search.length > 0) {
$('#l10n-client-string-select li').hide();
- $('#l10n-client-string-select li:contains('+search+')').show();
+ $('#l10n-client-string-select li').each(function() {
+ if ($(this).val().indexOf(search) != -1) {
+ $(this).show();
+ }
+ });
$('#l10n-client #g-l10n-search').val(search);
}
}
diff --git a/modules/gallery/libraries/Form_Uploadify.php b/modules/gallery/libraries/Form_Uploadify.php
index 56793c69..1e58018d 100644
--- a/modules/gallery/libraries/Form_Uploadify.php
+++ b/modules/gallery/libraries/Form_Uploadify.php
@@ -46,7 +46,7 @@ class Form_Uploadify_Core extends Form_Input {
$v->album = $this->data["album"];
$v->script_data = $this->data["script_data"];
$v->simultaneous_upload_limit = module::get_var("gallery", "simultaneous_upload_limit");
- $v->movies_allowed = (bool) movie::find_ffmpeg();
+ $v->movies_allowed = movie::allow_uploads();
$v->extensions = legal_file::get_filters();
$v->suhosin_session_encrypt = (bool) ini_get("suhosin.session.encrypt");
diff --git a/modules/gallery/libraries/Gallery_View.php b/modules/gallery/libraries/Gallery_View.php
index 64fea0ad..8f02b53c 100644
--- a/modules/gallery/libraries/Gallery_View.php
+++ b/modules/gallery/libraries/Gallery_View.php
@@ -82,8 +82,10 @@ class Gallery_View_Core extends View {
* @param $types a comma separated list of types to combine, eg "script,css"
*/
public function start_combining($types) {
- foreach (explode(",", $types) as $type) {
- $this->combine_queue[$type] = array();
+ if (gallery::allow_css_and_js_combining()) {
+ foreach (explode(",", $types) as $type) {
+ $this->combine_queue[$type] = array();
+ }
}
}
diff --git a/modules/gallery/libraries/MY_Database.php b/modules/gallery/libraries/MY_Database.php
index 604b6484..33759b67 100644
--- a/modules/gallery/libraries/MY_Database.php
+++ b/modules/gallery/libraries/MY_Database.php
@@ -32,6 +32,9 @@ abstract class Database extends Database_Core {
$config["connection"]["params"] = null;
}
parent::__construct($config);
+ if (gallery::show_profiler()) {
+ $this->config['benchmark'] = true;
+ }
}
/**
@@ -85,4 +88,14 @@ abstract class Database extends Database_Core {
static function set_default_instance($db) {
self::$instances["default"] = $db;
}
+
+ /**
+ * Escape LIKE queries, add wildcards. In MySQL queries using LIKE, _ and % characters are
+ * treated as wildcards similar to ? and *, respectively. Therefore, we need to escape _, %,
+ * and \ (the escape character itself).
+ */
+ static function escape_for_like($value) {
+ // backslash must go first to avoid double-escaping
+ return addcslashes($value, '\_%');
+ }
} \ No newline at end of file
diff --git a/modules/gallery/libraries/ORM_MPTT.php b/modules/gallery/libraries/ORM_MPTT.php
index ada8bfad..0ad81331 100644
--- a/modules/gallery/libraries/ORM_MPTT.php
+++ b/modules/gallery/libraries/ORM_MPTT.php
@@ -152,8 +152,9 @@ class ORM_MPTT_Core extends ORM {
*
* @return array ORM
*/
- function parents() {
+ function parents($where=null) {
return $this
+ ->merge_where($where)
->where("left_ptr", "<=", $this->left_ptr)
->where("right_ptr", ">=", $this->right_ptr)
->where("id", "<>", $this->id)
@@ -324,7 +325,7 @@ class ORM_MPTT_Core extends ORM {
* Lock the tree to prevent concurrent modification.
*/
protected function lock() {
- $timeout = module::get_var("gallery", "lock_timeout");
+ $timeout = module::get_var("gallery", "lock_timeout", 1);
$result = $this->db->query("SELECT GET_LOCK('{$this->table_name}', $timeout) AS l")->current();
if (empty($result->l)) {
throw new Exception("@todo UNABLE_TO_LOCK_EXCEPTION");
diff --git a/modules/gallery/libraries/Theme_View.php b/modules/gallery/libraries/Theme_View.php
index 8d69f23b..986fc8a2 100644
--- a/modules/gallery/libraries/Theme_View.php
+++ b/modules/gallery/libraries/Theme_View.php
@@ -86,6 +86,12 @@ class Theme_View_Core extends Gallery_View {
return $this->item;
}
+ public function siblings($limit=null, $offset=null) {
+ return call_user_func_array(
+ $this->siblings_callback[0],
+ array_merge($this->siblings_callback[1], array($offset, $limit)));
+ }
+
public function tag() {
return $this->tag;
}
diff --git a/modules/gallery/libraries/drivers/Cache/Database.php b/modules/gallery/libraries/drivers/Cache/Database.php
index a7aae92c..8790d0e1 100644
--- a/modules/gallery/libraries/drivers/Cache/Database.php
+++ b/modules/gallery/libraries/drivers/Cache/Database.php
@@ -69,7 +69,7 @@ class Cache_Database_Driver extends Cache_Driver {
->select()
->from("caches");
foreach ($tags as $tag) {
- $db->where("tags", "LIKE", "%<$tag>%");
+ $db->where("tags", "LIKE", "%" . Database::escape_for_like("<$tag>") . "%");
}
$db_result = $db->execute();
@@ -139,7 +139,7 @@ class Cache_Database_Driver extends Cache_Driver {
// Delete all caches
} else if ($is_tag === true) {
foreach ($keys as $tag) {
- $db->where("tags", "LIKE", "%<$tag>%");
+ $db->where("tags", "LIKE", "%" . Database::escape_for_like("<$tag>") . "%");
}
} else {
$db->where("key", "IN", $keys);
diff --git a/modules/gallery/models/item.php b/modules/gallery/models/item.php
index 85aca2c1..43b9a292 100644
--- a/modules/gallery/models/item.php
+++ b/modules/gallery/models/item.php
@@ -21,6 +21,7 @@ class Item_Model_Core extends ORM_MPTT {
protected $children = "items";
protected $sorting = array();
public $data_file = null;
+ private $data_file_error = null;
public function __construct($id=null) {
parent::__construct($id);
@@ -129,10 +130,11 @@ class Item_Model_Core extends ORM_MPTT {
/**
* Return the server-relative url to this item, eg:
- * /gallery3/index.php/BobsWedding?page=2
- * /gallery3/index.php/BobsWedding/Eating-Cake.jpg
+ * album: /gallery3/index.php/Bobs%20Wedding?page=2
+ * photo: /gallery3/index.php/Bobs%20Wedding/Eating-Cake
+ * movie: /gallery3/index.php/Bobs%20Wedding/First-Dance
*
- * @param string $query the query string (eg "show=3")
+ * @param string $query the query string (eg "page=2")
*/
public function url($query=null) {
$url = url::site($this->relative_url());
@@ -144,10 +146,11 @@ class Item_Model_Core extends ORM_MPTT {
/**
* Return the full url to this item, eg:
- * http://example.com/gallery3/index.php/BobsWedding?page=2
- * http://example.com/gallery3/index.php/BobsWedding/Eating-Cake.jpg
+ * album: http://example.com/gallery3/index.php/Bobs%20Wedding?page=2
+ * photo: http://example.com/gallery3/index.php/Bobs%20Wedding/Eating-Cake
+ * movie: http://example.com/gallery3/index.php/Bobs%20Wedding/First-Dance
*
- * @param string $query the query string (eg "show=3")
+ * @param string $query the query string (eg "page=2")
*/
public function abs_url($query=null) {
$url = url::abs_site($this->relative_url());
@@ -158,16 +161,24 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * album: /var/albums/album1/album2
- * photo: /var/albums/album1/album2/photo.jpg
+ * Return the full path to this item's file, eg:
+ * album: /usr/home/www/gallery3/var/albums/Bobs Wedding
+ * photo: /usr/home/www/gallery3/var/albums/Bobs Wedding/Eating-Cake.jpg
+ * movie: /usr/home/www/gallery3/var/albums/Bobs Wedding/First-Dance.mp4
*/
public function file_path() {
return VARPATH . "albums/" . urldecode($this->relative_path());
}
/**
- * album: http://example.com/gallery3/var/resizes/album1/
- * photo: http://example.com/gallery3/var/albums/album1/photo.jpg
+ * Return the relative url to this item's file, with cache buster, eg:
+ * album: var/albums/Bobs%20Wedding?m=1234567890
+ * photo: var/albums/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: var/albums/Bobs%20Wedding/First-Dance.mp4?m=1234567890
+ * If $full_uri==true, return the full url to this item's file, with cache buster, eg:
+ * album: http://example.com/gallery3/var/albums/Bobs%20Wedding?m=1234567890
+ * photo: http://example.com/gallery3/var/albums/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: http://example.com/gallery3/var/albums/Bobs%20Wedding/First-Dance.mp4?m=1234567890
*/
public function file_url($full_uri=false) {
$relative_path = "var/albums/" . $this->relative_path();
@@ -177,8 +188,10 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * album: /var/resizes/album1/.thumb.jpg
- * photo: /var/albums/album1/photo.thumb.jpg
+ * Return the full path to this item's thumb, eg:
+ * album: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/.album.jpg
+ * photo: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/Eating-Cake.jpg
+ * movie: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/First-Dance.jpg
*/
public function thumb_path() {
$base = VARPATH . "thumbs/" . urldecode($this->relative_path());
@@ -200,8 +213,14 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * album: http://example.com/gallery3/var/resizes/album1/.thumb.jpg
- * photo: http://example.com/gallery3/var/albums/album1/photo.thumb.jpg
+ * Return the relative url to this item's thumb, with cache buster, eg:
+ * album: var/thumbs/Bobs%20Wedding/.album.jpg?m=1234567890
+ * photo: var/thumbs/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: var/thumbs/Bobs%20Wedding/First-Dance.mp4?m=1234567890
+ * If $full_uri==true, return the full url to this item's file, with cache buster, eg:
+ * album: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/.album.jpg?m=1234567890
+ * photo: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/First-Dance.mp4?m=1234567890
*/
public function thumb_url($full_uri=false) {
$cache_buster = $this->_cache_buster($this->thumb_path());
@@ -219,8 +238,11 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * album: /var/resizes/album1/.resize.jpg
- * photo: /var/albums/album1/photo.resize.jpg
+ * Return the full path to this item's resize, eg:
+ * album: /usr/home/www/gallery3/var/resizes/Bobs Wedding/.album.jpg (*)
+ * photo: /usr/home/www/gallery3/var/resizes/Bobs Wedding/Eating-Cake.jpg
+ * movie: /usr/home/www/gallery3/var/resizes/Bobs Wedding/First-Dance.mp4 (*)
+ * (*) Since only photos have resizes, album and movie paths are fictitious.
*/
public function resize_path() {
return VARPATH . "resizes/" . urldecode($this->relative_path()) .
@@ -228,8 +250,15 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * album: http://example.com/gallery3/var/resizes/album1/.resize.jpg
- * photo: http://example.com/gallery3/var/albums/album1/photo.resize.jpg
+ * Return the relative url to this item's resize, with cache buster, eg:
+ * album: var/resizes/Bobs%20Wedding/.album.jpg?m=1234567890 (*)
+ * photo: var/resizes/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: var/resizes/Bobs%20Wedding/First-Dance.mp4?m=1234567890 (*)
+ * If $full_uri==true, return the full url to this item's file, with cache buster, eg:
+ * album: http://example.com/gallery3/var/resizes/Bobs%20Wedding/.album.jpg?m=1234567890 (*)
+ * photo: http://example.com/gallery3/var/resizes/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890
+ * movie: http://example.com/gallery3/var/resizes/Bobs%20Wedding/First-Dance.mp4?m=1234567890 (*)
+ * (*) Since only photos have resizes, album and movie urls are fictitious.
*/
public function resize_url($full_uri=false) {
$relative_path = "var/resizes/" . $this->relative_path();
@@ -318,9 +347,11 @@ class Item_Model_Core extends ORM_MPTT {
*/
public function save() {
$significant_changes = $this->changed;
- unset($significant_changes["view_count"]);
- unset($significant_changes["relative_url_cache"]);
- unset($significant_changes["relative_path_cache"]);
+ foreach (array("view_count", "relative_url_cache", "relative_path_cache",
+ "resize_width", "resize_height", "resize_dirty",
+ "thumb_width", "thumb_height", "thumb_dirty") as $key) {
+ unset($significant_changes[$key]);
+ }
if ((!empty($this->changed) && $significant_changes) || isset($this->data_file)) {
$this->updated = time();
@@ -334,35 +365,27 @@ class Item_Model_Core extends ORM_MPTT {
$this->weight = item::get_max_weight();
}
+ // Process the data file info.
+ if (isset($this->data_file)) {
+ $this->_process_data_file_info();
+ } else if (!$this->is_album()) {
+ // Unless it's an album, new items must have a data file.
+ $this->data_file_error = true;
+ }
+
// Make an url friendly slug from the name, if necessary
if (empty($this->slug)) {
$this->slug = item::convert_filename_to_slug(pathinfo($this->name, PATHINFO_FILENAME));
- // If the filename is all invalid characters, then the slug may be empty here. Pick a
- // random value.
+ // If the filename is all invalid characters, then the slug may be empty here. We set a
+ // generic name ("photo", "movie", or "album") based on its type, then rely on
+ // check_and_fix_conflicts to ensure it doesn't conflict with another name.
if (empty($this->slug)) {
- $this->slug = (string)rand(1000, 9999);
- }
- }
-
- // Get the width, height and mime type from our data file for photos and movies.
- if ($this->is_photo() || $this->is_movie()) {
- if ($this->is_photo()) {
- list ($this->width, $this->height, $this->mime_type, $extension) =
- photo::get_file_metadata($this->data_file);
- } else if ($this->is_movie()) {
- list ($this->width, $this->height, $this->mime_type, $extension) =
- movie::get_file_metadata($this->data_file);
- }
-
- // Force an extension onto the name if necessary
- $pi = pathinfo($this->data_file);
- if (empty($pi["extension"])) {
- $this->name = "{$this->name}.$extension";
+ $this->slug = $this->type;
}
}
- $this->_randomize_name_or_slug_on_conflict();
+ $this->_check_and_fix_conflicts();
parent::save();
@@ -382,16 +405,6 @@ class Item_Model_Core extends ORM_MPTT {
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"] . "-" . random::int() . "." . $pi["extension"];
- parent::save();
- }
-
copy($this->data_file, $this->file_path());
break;
}
@@ -409,24 +422,28 @@ class Item_Model_Core extends ORM_MPTT {
// keep it around.
$original = ORM::factory("item", $this->id);
- // Preserve the extension of the data file. Many helpers, (e.g. ImageMagick), assume
+ // If we have a new data file, process its info. This will get its metadata and
+ // preserve the extension of the data file. Many helpers, (e.g. ImageMagick), assume
// the MIME type from the extension. So when we adopt the new data file, it's important
// to adopt the new extension. That ensures that the item's extension is always
// appropriate for its data. We don't try to preserve the name of the data file, though,
// because the name is typically a temporary randomly-generated name.
if (isset($this->data_file)) {
- $extension = pathinfo($this->data_file, PATHINFO_EXTENSION);
- $new_name = pathinfo($this->name, PATHINFO_FILENAME) . ".$extension";
- if (!empty($extension) && strcmp($this->name, $new_name)) {
- $this->name = $new_name;
- }
- if ($this->is_photo()) {
- list ($this->width, $this->height, $this->mime_type, $extension) =
- photo::get_file_metadata($this->data_file);
- } else if ($this->is_movie()) {
- list ($this->width, $this->height, $this->mime_type, $extension) =
- movie::get_file_metadata($this->data_file);
- }
+ $this->_process_data_file_info();
+ } else if (!$this->is_album() && array_key_exists("name", $this->changed)) {
+ // There's no new data file, but the name changed. If it's a photo or movie,
+ // make sure the new name still agrees with the file type.
+ $this->name = legal_file::sanitize_filename($this->name,
+ pathinfo($original->name, PATHINFO_EXTENSION), $this->type);
+ }
+
+ // If an album's cover has changed (or been removed), delete any existing album cover,
+ // reset the thumb metadata, and mark the thumb as dirty.
+ if (array_key_exists("album_cover_item_id", $this->changed) && $this->is_album()) {
+ @unlink($original->thumb_path());
+ $this->thumb_dirty = 1;
+ $this->thumb_height = 0;
+ $this->thumb_width = 0;
}
if (array_intersect($this->changed, array("parent_id", "name", "slug"))) {
@@ -435,7 +452,7 @@ class Item_Model_Core extends ORM_MPTT {
$this->relative_url_cache = null;
}
- $this->_randomize_name_or_slug_on_conflict();
+ $this->_check_and_fix_conflicts();
parent::save();
@@ -494,13 +511,6 @@ class Item_Model_Core extends ORM_MPTT {
// Replace the data file, if requested.
if ($this->data_file && ($this->is_photo() || $this->is_movie())) {
copy($this->data_file, $this->file_path());
-
- // Get the width, height and mime type from our data file for photos and movies.
- if ($this->is_photo()) {
- list ($this->width, $this->height) = photo::get_file_metadata($this->file_path());
- } else if ($this->is_movie()) {
- list ($this->width, $this->height) = movie::get_file_metadata($this->file_path());
- }
$this->thumb_dirty = 1;
$this->resize_dirty = 1;
}
@@ -528,30 +538,94 @@ class Item_Model_Core extends ORM_MPTT {
/**
* Check to see if there's another item that occupies the same name or slug that this item
- * intends to use, and if so choose a new name/slug while preserving the extension.
- * @todo Improve this. Random numbers are not user friendly
+ * intends to use, and if so choose a new name/slug while preserving the extension. Since this
+ * checks the name without its extension, it covers possible collisions with thumbs and resizes
+ * as well (e.g. between the thumbs of movie "foo.flv" and photo "foo.jpg").
*/
- private function _randomize_name_or_slug_on_conflict() {
- $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)
- ->where("id", $this->id ? "<>" : "IS NOT", $this->id)
- ->and_open()
- ->where("name", "=", $this->name)
- ->or_where("slug", "=", $this->slug)
- ->close()
- ->find()->id) {
- $rand = random::int();
- if ($base_ext) {
- $this->name = "$base_name-$rand.$base_ext";
+ private function _check_and_fix_conflicts() {
+ $suffix_num = 1;
+ $suffix = "";
+ if ($this->is_album()) {
+ while (db::build()
+ ->from("items")
+ ->where("parent_id", "=", $this->parent_id)
+ ->where("id", $this->id ? "<>" : "IS NOT", $this->id)
+ ->and_open()
+ ->where("name", "=", "{$this->name}{$suffix}")
+ ->or_where("slug", "=", "{$this->slug}{$suffix}")
+ ->close()
+ ->count_records()) {
+ $suffix = "-" . (($suffix_num <= 99) ? sprintf("%02d", $suffix_num++) : random::int());
+ }
+ if ($suffix) {
+ $this->name = "{$this->name}{$suffix}";
+ $this->slug = "{$this->slug}{$suffix}";
+ $this->relative_path_cache = null;
+ $this->relative_url_cache = null;
+ }
+ } else {
+ // Split the filename into its base and extension. This uses a regexp similar to
+ // legal_file::change_extension (which isn't always the same as pathinfo).
+ if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $this->name, $matches)) {
+ $base_name = $matches[1];
+ $extension = $matches[2]; // includes a leading dot
} else {
- $this->name = "$base_name-$rand";
+ $base_name = $this->name;
+ $extension = "";
+ }
+ $base_name_escaped = Database::escape_for_like($base_name);
+ // Note: below query uses LIKE with wildcard % at end, which is still sargable (i.e. quick)
+ while (db::build()
+ ->from("items")
+ ->where("parent_id", "=", $this->parent_id)
+ ->where("id", $this->id ? "<>" : "IS NOT", $this->id)
+ ->and_open()
+ ->where("name", "LIKE", "{$base_name_escaped}{$suffix}.%")
+ ->or_where("slug", "=", "{$this->slug}{$suffix}")
+ ->close()
+ ->count_records()) {
+ $suffix = "-" . (($suffix_num <= 99) ? sprintf("%02d", $suffix_num++) : random::int());
+ }
+ if ($suffix) {
+ $this->name = "{$base_name}{$suffix}{$extension}";
+ $this->slug = "{$this->slug}{$suffix}";
+ $this->relative_path_cache = null;
+ $this->relative_url_cache = null;
}
- $this->slug = "$base_slug-$rand";
- $this->relative_path_cache = null;
- $this->relative_url_cache = null;
+ }
+ }
+
+ /**
+ * Process the data file info. Get its metadata and extension.
+ * If valid, use it to sanitize the item name and update the
+ * width, height, and mime type.
+ */
+ private function _process_data_file_info() {
+ try {
+ if ($this->is_photo()) {
+ list ($this->width, $this->height, $this->mime_type, $extension) =
+ photo::get_file_metadata($this->data_file);
+ } else if ($this->is_movie()) {
+ list ($this->width, $this->height, $this->mime_type, $extension) =
+ movie::get_file_metadata($this->data_file);
+ } else {
+ // Albums don't have data files.
+ $this->data_file = null;
+ return;
+ }
+
+ // Sanitize the name based on the idenified extension, but only set $this->name if different
+ // to ensure it isn't unnecessarily marked as "changed"
+ $name = legal_file::sanitize_filename($this->name, $extension, $this->type);
+ if ($this->name != $name) {
+ $this->name = $name;
+ }
+
+ // Data file valid - make sure the flag is reset to false.
+ $this->data_file_error = false;
+ } catch (Exception $e) {
+ // Data file invalid - set the flag so it's reported during item validation.
+ $this->data_file_error = true;
}
}
@@ -663,7 +737,7 @@ class Item_Model_Core extends ORM_MPTT {
}
/**
- * Return a view for movies. By default this is a Flowplayer v3 <script> tag, but
+ * Return a view for movies. By default this is a Flowplayer v3 <script> tag, but
* movie_img events can override this and provide their own player/view. If no player/view
* is found and the movie is unsupported by Flowplayer v3, this returns a simple download link.
* @param array $extra_attrs
@@ -675,12 +749,12 @@ class Item_Model_Core extends ORM_MPTT {
$height = $this->height;
if ($width == 0 || $height == 0) {
// Not set correctly, likely because ffmpeg isn't available. Making the window 0x0 causes the
- // video to be effectively unviewable. So, let's guess: set width to max_size and guess a
- // height (using 4:3 aspect ratio). Once the video metadata is loaded, js in
+ // video to be effectively unviewable. So, let's guess: set width to max_size and guess a
+ // height (using 4:3 aspect ratio). Once the video metadata is loaded, js in
// movieplayer.html.php will correct these values.
$width = $max_size;
$height = ceil($width * 3/4);
- }
+ }
$attrs = array_merge(array("id" => "g-item-id-{$this->id}"), $extra_attrs,
array("class" => "g-movie"));
@@ -858,27 +932,38 @@ class Item_Model_Core extends ORM_MPTT {
return;
}
- if ($this->is_photo()) {
- if (!in_array(strtolower($ext), legal_file::get_photo_extensions())) {
- $v->add_error("name", "illegal_data_file_extension");
- }
- }
-
- if ($this->is_movie()) {
- if (!in_array(strtolower($ext), legal_file::get_movie_extensions())) {
- $v->add_error("name", "illegal_data_file_extension");
- }
+ if ($this->is_photo() && !legal_file::get_photo_extensions($ext) ||
+ $this->is_movie() && !legal_file::get_movie_extensions($ext)) {
+ $v->add_error("name", "illegal_data_file_extension");
}
}
- 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;
+ if ($this->is_album()) {
+ 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;
+ }
+ } else {
+ if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $this->name, $matches)) {
+ $base_name = $matches[1];
+ } else {
+ $base_name = $this->name;
+ }
+ $base_name_escaped = Database::escape_for_like($base_name);
+ if (db::build()
+ ->from("items")
+ ->where("parent_id", "=", $this->parent_id)
+ ->where("name", "LIKE", "{$base_name_escaped}.%")
+ ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null)
+ ->count_records()) {
+ $v->add_error("name", "conflict");
+ return;
+ }
}
if ($this->parent_id == 1 && Kohana::auto_load("{$this->slug}_Controller")) {
@@ -895,6 +980,8 @@ class Item_Model_Core extends ORM_MPTT {
$v->add_error("name", "bad_data_file_path");
} else if (filesize($this->data_file) == 0) {
$v->add_error("name", "empty_data_file");
+ } else if ($this->data_file_error) {
+ $v->add_error("name", "invalid_data_file");
}
}
@@ -934,10 +1021,12 @@ class Item_Model_Core extends ORM_MPTT {
return;
}
- if ($this->album_cover_item_id && db::build()
+ if ($this->album_cover_item_id && ($this->is_photo() || $this->is_movie() ||
+ db::build()
->from("items")
->where("id", "=", $this->album_cover_item_id)
- ->count_records() != 1) {
+ ->where("type", "<>", "album")
+ ->count_records() != 1)) {
$v->add_error("album_cover_item_id", "invalid_item");
}
}
@@ -1073,6 +1162,10 @@ class Item_Model_Core extends ORM_MPTT {
$data["can_edit"] = access::can("edit", $this);
}
+ if (empty($fields) || isset($fields["can_add"])) {
+ $data["can_add"] = access::can("add", $this);
+ }
+
// Elide some internal-only data that is going to cause confusion in the client.
foreach (array("relative_path_cache", "relative_url_cache", "left_ptr", "right_ptr",
"thumb_dirty", "resize_dirty", "weight") as $key) {
diff --git a/modules/gallery/module.info b/modules/gallery/module.info
index 566ca2eb..2383ec3c 100644
--- a/modules/gallery/module.info
+++ b/modules/gallery/module.info
@@ -1,6 +1,6 @@
name = "Gallery 3"
description = "Gallery core application"
-version = 53
+version = 56
author_name = "Gallery Team"
author_url = "http://codex.galleryproject.org/Gallery:Team"
info_url = "http://codex.galleryproject.org/Gallery3:Modules:gallery"
diff --git a/modules/gallery/tests/Data_Rest_Helper_Test.php b/modules/gallery/tests/Data_Rest_Helper_Test.php
index 69d17997..e6a94864 100644
--- a/modules/gallery/tests/Data_Rest_Helper_Test.php
+++ b/modules/gallery/tests/Data_Rest_Helper_Test.php
@@ -99,4 +99,13 @@ class Data_Rest_Helper_Test extends Gallery_Unit_Test_Case {
// pass
}
}
+
+ public function cache_buster_test() {
+ $photo = test::random_photo();
+
+ $this->assert_same(
+ url::abs_site("rest/data/{$photo->id}?size=thumb&m=" . filemtime($photo->thumb_path())),
+ data_rest::url($photo, "thumb"));
+ }
}
+
diff --git a/modules/gallery/tests/Database_Test.php b/modules/gallery/tests/Database_Test.php
index ab3290a9..106062f5 100644
--- a/modules/gallery/tests/Database_Test.php
+++ b/modules/gallery/tests/Database_Test.php
@@ -147,6 +147,12 @@ class Database_Test extends Gallery_Unit_Test_Case {
$sql = str_replace("\n", " ", $sql);
$this->assert_same("UPDATE [test_tables] SET [name] = [Test Name] WHERE [1] = [1]", $sql);
}
+
+ function escape_for_like_test() {
+ // Note: literal double backslash is written as \\\
+ $this->assert_same('basic\_test', Database::escape_for_like("basic_test"));
+ $this->assert_same('\\\100\%\_test/', Database::escape_for_like('\100%_test/'));
+ }
}
class Database_Mock extends Database {
diff --git a/modules/gallery/tests/File_Structure_Test.php b/modules/gallery/tests/File_Structure_Test.php
index 8f6e480c..ce75ea13 100644
--- a/modules/gallery/tests/File_Structure_Test.php
+++ b/modules/gallery/tests/File_Structure_Test.php
@@ -283,4 +283,52 @@ class File_Structure_Test extends Gallery_Unit_Test_Case {
$this->assert_true(false, $errors);
}
}
+
+ public function all_public_functions_in_test_files_end_in_test() {
+ // Who tests the tests? :-) (ref: http://www.xkcd.com/1163)
+ $dir = new PhpCodeFilterIterator(
+ new GalleryCodeFilterIterator(
+ new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(DOCROOT))));
+ foreach ($dir as $file) {
+ $scan = 0;
+ if (basename(dirname($file)) == "tests") {
+ foreach (file($file) as $line) {
+ if (!substr($file, -9, 9) == "_Test.php") {
+ continue;
+ }
+
+ if (preg_match("/class.*extends.*Gallery_Unit_Test_Case/", $line)) {
+ $scan = 1;
+ } else if (preg_match("/class.*extends/", $line)) {
+ $scan = 0;
+ }
+
+ if ($scan) {
+ if (preg_match("/^\s*public\s+function/", $line)) {
+ $this->assert_true(
+ preg_match("/^\s*public\s+function (setup|teardown|.*_test)\(\) {/", $line),
+ "public functions must end in _test:\n$file\n$line\n");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public function no_extra_spaces_at_end_of_line_test() {
+ $dir = new GalleryCodeFilterIterator(
+ new RecursiveIteratorIterator(new RecursiveDirectoryIterator(DOCROOT)));
+ $errors = "";
+ foreach ($dir as $file) {
+ if (preg_match("/\.(php|css|html|js)$/", $file)) {
+ foreach (file($file) as $line_num => $line) {
+ if ((substr($line, -2) == " \n") || (substr($line, -1) == " ")) {
+ $errors .= "$file at line " . ($line_num + 1) . "\n";
+ }
+ }
+ }
+ }
+ $this->assert_true(empty($errors), "Extra spaces at end of line found at:\n$errors");
+ }
}
diff --git a/modules/gallery/tests/Gallery_Filters.php b/modules/gallery/tests/Gallery_Filters.php
index 7209bc93..6c2a6aa3 100644
--- a/modules/gallery/tests/Gallery_Filters.php
+++ b/modules/gallery/tests/Gallery_Filters.php
@@ -26,7 +26,7 @@ class PhpCodeFilterIterator extends FilterIterator {
class GalleryCodeFilterIterator extends FilterIterator {
public function accept() {
- // Skip anything that we didn"t write
+ // Skip anything that we didn't write
$path_name = $this->getInnerIterator()->getPathName();
$file_name = $this->getInnerIterator()->getFileName();
return !(
@@ -47,6 +47,10 @@ class GalleryCodeFilterIterator extends FilterIterator {
strpos($path_name, SYSPATH) !== false ||
strpos($path_name, MODPATH . "gallery/libraries/HTMLPurifier") !== false ||
strpos($path_name, MODPATH . "gallery/vendor/joomla") !== false ||
+ strpos($path_name, MODPATH . "organize/vendor/ext") !== false ||
+ strpos($path_name, DOCROOT . "lib") !== false ||
+ strpos($path_name, DOCROOT . "themes/admin_wind/css/themeroller") !== false ||
+ strpos($path_name, DOCROOT . "themes/wind/css/themeroller") !== false ||
substr($path_name, -1, 1) == "~");
}
}
diff --git a/modules/gallery/tests/Gallery_Graphics_Helper_Test.php b/modules/gallery/tests/Gallery_Graphics_Helper_Test.php
new file mode 100644
index 00000000..20096b23
--- /dev/null
+++ b/modules/gallery/tests/Gallery_Graphics_Helper_Test.php
@@ -0,0 +1,137 @@
+<?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 Gallery_Graphics_Helper_Test extends Gallery_Unit_Test_Case {
+ public function rotate_jpg_test() {
+ // Input is a 1024x768 jpg, output is rotated 90 degrees
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ $options = array("degrees" => 90);
+ gallery_graphics::rotate($input_file, $output_file, $options, null);
+
+ // Output is rotated to 768x1024 jpg
+ $this->assert_equal(array(768, 1024, "image/jpeg", "jpg"), photo::get_file_metadata($output_file));
+ }
+
+ public function rotate_jpg_without_options_test() {
+ // Input is a 1024x768 jpg, output options undefined
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ gallery_graphics::rotate($input_file, $output_file, null, null);
+
+ // Output is not rotated, still a 1024x768 jpg
+ $this->assert_equal(array(1024, 768, "image/jpeg", "jpg"), photo::get_file_metadata($output_file));
+ }
+
+ public function rotate_bad_jpg_test() {
+ // Input is a garbled jpg, output is jpg autofit to 300x300
+ $input_file = TMPPATH . test::random_name() . ".jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ $options = array("degrees" => 90);
+ file_put_contents($input_file, test::lorem_ipsum(200));
+
+ // Should get passed to Image library and throw an exception
+ try {
+ gallery_graphics::rotate($input_file, $output_file, $options, null);
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function resize_jpg_test() {
+ // Input is a 1024x768 jpg, output is jpg autofit to 300x300
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ $options = array("width" => 300, "height" => 300, "master" => Image::AUTO);
+ gallery_graphics::resize($input_file, $output_file, $options, null);
+
+ // Output is resized to 300x225 jpg
+ $this->assert_equal(array(300, 225, "image/jpeg", "jpg"), photo::get_file_metadata($output_file));
+ }
+
+ public function resize_jpg_to_png_test() {
+ // Input is a 1024x768 jpg, output is png autofit to 300x300
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".png";
+ $options = array("width" => 300, "height" => 300, "master" => Image::AUTO);
+ gallery_graphics::resize($input_file, $output_file, $options, null);
+
+ // Output is resized to 300x225 png
+ $this->assert_equal(array(300, 225, "image/png", "png"), photo::get_file_metadata($output_file));
+ }
+
+ public function resize_jpg_with_no_upscale_test() {
+ // Input is a 1024x768 jpg, output is jpg autofit to 1200x1200 - should not upscale
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ $options = array("width" => 1200, "height" => 1200, "master" => Image::AUTO);
+ gallery_graphics::resize($input_file, $output_file, $options, null);
+
+ // Output is copied directly from input
+ $this->assert_equal(file_get_contents($input_file), file_get_contents($output_file));
+ }
+
+ public function resize_jpg_to_png_with_no_upscale_test() {
+ // Input is a 1024x768 jpg, output is png autofit to 1200x1200 - should not upscale
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".png";
+ $options = array("width" => 1200, "height" => 1200, "master" => Image::AUTO);
+ gallery_graphics::resize($input_file, $output_file, $options, null);
+
+ // Output is converted from input without resize
+ $this->assert_equal(array(1024, 768, "image/png", "png"), photo::get_file_metadata($output_file));
+ }
+
+ public function resize_jpg_without_options_test() {
+ // Input is a 1024x768 jpg, output is jpg without options - should not attempt resize
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ gallery_graphics::resize($input_file, $output_file, null, null);
+
+ // Output is copied directly from input
+ $this->assert_equal(file_get_contents($input_file), file_get_contents($output_file));
+ }
+
+ public function resize_jpg_to_png_without_options_test() {
+ // Input is a 1024x768 jpg, output is png without options - should not attempt resize
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".png";
+ gallery_graphics::resize($input_file, $output_file, null, null);
+
+ // Output is converted from input without resize
+ $this->assert_equal(array(1024, 768, "image/png", "png"), photo::get_file_metadata($output_file));
+ }
+
+ public function resize_bad_jpg_test() {
+ // Input is a garbled jpg, output is jpg autofit to 300x300
+ $input_file = TMPPATH . test::random_name() . ".jpg";
+ $output_file = TMPPATH . test::random_name() . ".jpg";
+ $options = array("width" => 300, "height" => 300, "master" => Image::AUTO);
+ file_put_contents($input_file, test::lorem_ipsum(200));
+
+ // Should get passed to Image library and throw an exception
+ try {
+ gallery_graphics::resize($input_file, $output_file, $options, null);
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/tests/Graphics_Helper_Test.php b/modules/gallery/tests/Graphics_Helper_Test.php
new file mode 100644
index 00000000..2cf5caa7
--- /dev/null
+++ b/modules/gallery/tests/Graphics_Helper_Test.php
@@ -0,0 +1,158 @@
+<?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 Graphics_Helper_Test extends Gallery_Unit_Test_Case {
+ public function generate_photo_test() {
+ $photo = test::random_photo();
+ // Check that the images were correctly resized
+ $this->assert_equal(array(640, 480, "image/jpeg", "jpg"),
+ photo::get_file_metadata($photo->resize_path()));
+ $this->assert_equal(array(200, 150, "image/jpeg", "jpg"),
+ photo::get_file_metadata($photo->thumb_path()));
+ // Check that the items table got updated
+ $this->assert_equal(array(640, 480), array($photo->resize_width, $photo->resize_height));
+ $this->assert_equal(array(200, 150), array($photo->thumb_width, $photo->thumb_height));
+ // Check that the images are not marked dirty
+ $this->assert_equal(0, $photo->resize_dirty);
+ $this->assert_equal(0, $photo->thumb_dirty);
+ }
+
+ public function generate_movie_test() {
+ $movie = test::random_movie();
+ // Check that the image was correctly resized
+ $this->assert_equal(array(200, 160, "image/jpeg", "jpg"),
+ photo::get_file_metadata($movie->thumb_path()));
+ // Check that the items table got updated
+ $this->assert_equal(array(200, 160), array($movie->thumb_width, $movie->thumb_height));
+ // Check that the image is not marked dirty
+ $this->assert_equal(0, $movie->thumb_dirty);
+ }
+
+ public function generate_album_cover_test() {
+ $album = test::random_album();
+ $photo = test::random_unique_photo($album);
+ $album->reload();
+ // Check that the image was copied directly from item thumb
+ $this->assert_equal(file_get_contents($photo->thumb_path()),
+ file_get_contents($album->thumb_path()));
+ // Check that the items table got updated
+ $this->assert_equal(array(200, 150), array($album->thumb_width, $album->thumb_height));
+ // Check that the image is not marked dirty
+ $this->assert_equal(0, $album->thumb_dirty);
+ }
+
+ public function generate_album_cover_from_png_test() {
+ $input_file = MODPATH . "gallery/tests/test.jpg";
+ $output_file = TMPPATH . test::random_name() . ".png";
+ gallery_graphics::resize($input_file, $output_file, null, null);
+
+ $album = test::random_album();
+ $photo = test::random_photo_unsaved($album);
+ $photo->set_data_file($output_file);
+ $photo->name = "album_cover_from_png.png";
+ $photo->save();
+ $album->reload();
+ // Check that the image was correctly resized and converted to jpg
+ $this->assert_equal(array(200, 150, "image/jpeg", "jpg"),
+ photo::get_file_metadata($album->thumb_path()));
+ // Check that the items table got updated
+ $this->assert_equal(array(200, 150), array($album->thumb_width, $album->thumb_height));
+ // Check that the image is not marked dirty
+ $this->assert_equal(0, $album->thumb_dirty);
+ }
+
+ public function generate_album_cover_for_empty_album_test() {
+ $album = test::random_album();
+ // Check that the album cover is the missing image placeholder
+ $this->assert_same(file_get_contents(MODPATH . "gallery/images/missing_album_cover.jpg"),
+ file_get_contents($album->thumb_path()));
+ // Check that the items table got updated with new metadata
+ $this->assert_equal(array(200, 200), array($album->thumb_width, $album->thumb_height));
+ // Check that the image is *not* marked as dirty
+ $this->assert_equal(0, $album->thumb_dirty);
+ }
+
+ public function generate_bad_photo_test() {
+ $photo = test::random_photo();
+ // At this point, the photo is valid and has a valid resize and thumb. Make it garble.
+ file_put_contents($photo->file_path(), test::lorem_ipsum(200));
+ // Regenerate
+ $photo->resize_dirty = 1;
+ $photo->thumb_dirty = 1;
+ try {
+ graphics::generate($photo);
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // Exception expected
+ }
+ // Check that the images got replaced with missing image placeholders
+ $this->assert_same(file_get_contents(MODPATH . "gallery/images/missing_photo.jpg"),
+ file_get_contents($photo->resize_path()));
+ $this->assert_same(file_get_contents(MODPATH . "gallery/images/missing_photo.jpg"),
+ file_get_contents($photo->thumb_path()));
+ // Check that the items table got updated with new metadata
+ $this->assert_equal(array(200, 200), array($photo->resize_width, $photo->resize_height));
+ $this->assert_equal(array(200, 200), array($photo->thumb_width, $photo->thumb_height));
+ // Check that the images are marked as dirty
+ $this->assert_equal(1, $photo->resize_dirty);
+ $this->assert_equal(1, $photo->thumb_dirty);
+ }
+
+ public function generate_bad_movie_test() {
+ // Unlike photos, its ok to have missing movies - no thrown exceptions, thumb_dirty can be reset.
+ $movie = test::random_movie();
+ // At this point, the movie is valid and has a valid thumb. Make it garble.
+ file_put_contents($movie->file_path(), test::lorem_ipsum(200));
+ // Regenerate
+ $movie->thumb_dirty = 1;
+ graphics::generate($movie);
+ // Check that the image got replaced with a missing image placeholder
+ $this->assert_same(file_get_contents(MODPATH . "gallery/images/missing_movie.jpg"),
+ file_get_contents($movie->thumb_path()));
+ // Check that the items table got updated with new metadata
+ $this->assert_equal(array(200, 200), array($movie->thumb_width, $movie->thumb_height));
+ // Check that the image is *not* marked as dirty
+ $this->assert_equal(0, $movie->thumb_dirty);
+ }
+
+ public function generate_album_cover_from_bad_photo_test() {
+ $album = test::random_album();
+ $photo = test::random_photo($album);
+ $album->reload();
+ // At this point, the photo is valid and has a valid resize and thumb. Make it garble.
+ file_put_contents($photo->file_path(), test::lorem_ipsum(200));
+ // Regenerate album from garbled photo.
+ $photo->thumb_dirty = 1;
+ $photo->save();
+ $album->thumb_dirty = 1;
+ try {
+ graphics::generate($album);
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // Exception expected
+ }
+ // Check that the image got replaced with a missing image placeholder
+ $this->assert_same(file_get_contents(MODPATH . "gallery/images/missing_photo.jpg"),
+ file_get_contents($album->thumb_path()));
+ // Check that the items table got updated with new metadata
+ $this->assert_equal(array(200, 200), array($album->thumb_width, $album->thumb_height));
+ // Check that the images are marked as dirty
+ $this->assert_equal(1, $album->thumb_dirty);
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/tests/Item_Helper_Test.php b/modules/gallery/tests/Item_Helper_Test.php
index 0c08d1af..f5b99bec 100644
--- a/modules/gallery/tests/Item_Helper_Test.php
+++ b/modules/gallery/tests/Item_Helper_Test.php
@@ -235,4 +235,22 @@ class Item_Helper_Test extends Gallery_Unit_Test_Case {
$level3b->id,
item::find_by_relative_url("{$level1->slug}/{$level2b->slug}/{$level3b->slug}")->id);
}
+
+ public function resequence_child_weights_test() {
+ $album = test::random_album_unsaved();
+ $album->sort_column = "id";
+ $album->save();
+
+ $photo1 = test::random_photo($album);
+ $photo2 = test::random_photo($album);
+ $this->assert_true($photo2->weight > $photo1->weight);
+
+ $album->reload();
+ $album->sort_order = "DESC";
+ $album->save();
+ item::resequence_child_weights($album);
+
+ $this->assert_equal(2, $photo1->reload()->weight);
+ $this->assert_equal(1, $photo2->reload()->weight);
+ }
}
diff --git a/modules/gallery/tests/Item_Model_Test.php b/modules/gallery/tests/Item_Model_Test.php
index dc4432a6..fcb5c2ad 100644
--- a/modules/gallery/tests/Item_Model_Test.php
+++ b/modules/gallery/tests/Item_Model_Test.php
@@ -66,7 +66,7 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
}
public function rename_photo_test() {
- $item = test::random_photo();
+ $item = test::random_unique_photo();
$original_name = $item->name;
$thumb_file = file_get_contents($item->thumb_path());
@@ -89,7 +89,7 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
public function rename_album_test() {
$album = test::random_album();
- $photo = test::random_photo($album);
+ $photo = test::random_unique_photo($album);
$album->reload();
$thumb_file = file_get_contents($photo->thumb_path());
@@ -126,33 +126,15 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
public function item_rename_wont_accept_slash_test() {
$item = test::random_photo();
- try {
- $item->name = test::random_name() . "/";
- $item->save();
- } catch (ORM_Validation_Exception $e) {
- $this->assert_equal(array("name" => "no_slashes"), $e->validation->errors());
- return;
- }
- $this->assert_true(false, "Shouldn't get here");
- }
-
- public function item_rename_over_existing_name_gets_uniqified_test() {
- // Create a test photo
- $item = test::random_photo();
- $item2 = test::random_photo();
-
- $item->name = $item2->name;
+ $item->name = "/no_slashes/allowed/";
$item->save();
-
- // foo.jpg should become foo-####.jpg
- $this->assert_true(
- preg_match("/" . str_replace(".jpg", "", $item2->name) . "-\d+\.jpg/", $item->name));
+ $this->assert_equal("no_slashes_allowed.jpg", $item->name);
}
public function move_album_test() {
$album2 = test::random_album();
$album1 = test::random_album($album2);
- $photo = test::random_photo($album1);
+ $photo = test::random_unique_photo($album1);
$thumb_file = file_get_contents($photo->thumb_path());
$resize_file = file_get_contents($photo->resize_path());
@@ -180,7 +162,7 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
public function move_photo_test() {
$album1 = test::random_album();
- $photo = test::random_photo($album1);
+ $photo = test::random_unique_photo($album1);
$album2 = test::random_album();
@@ -205,7 +187,7 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$this->assert_equal($fullsize_file, file_get_contents($photo->file_path()));
}
- public function move_album_with_conflicting_target_gets_uniqified_test() {
+ public function move_album_with_conflicting_target_gets_uniquified_test() {
$album = test::random_album();
$source = test::random_album_unsaved($album);
$source->name = $album->name;
@@ -217,9 +199,9 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$source->parent_id = item::root()->id;
$source->save();
- // foo should become foo-####
- $this->assert_true(preg_match("/{$album->name}-\d+/", $source->name));
- $this->assert_true(preg_match("/{$album->slug}-\d+/", $source->slug));
+ // foo should become foo-01
+ $this->assert_same("{$album->name}-01", $source->name);
+ $this->assert_same("{$album->slug}-01", $source->slug);
}
public function move_album_fails_wrong_target_type_test() {
@@ -239,7 +221,7 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$this->assert_true(false, "Shouldn't get here");
}
- public function move_photo_with_conflicting_target_gets_uniqified_test() {
+ public function move_photo_with_conflicting_target_gets_uniquified_test() {
$photo1 = test::random_photo();
$album = test::random_album();
$photo2 = test::random_photo_unsaved($album);
@@ -247,17 +229,16 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$photo2->save();
// $photo1 and $photo2 have the same name, so if we move $photo1 into the root they should
- // conflict and get uniqified.
+ // conflict and get uniquified.
$photo2->parent_id = item::root()->id;
$photo2->save();
- // foo.jpg should become foo-####.jpg
- $this->assert_true(
- preg_match("/" . str_replace(".jpg", "", $photo1->name) . "-\d+\.jpg/", $photo2->name));
+ // foo.jpg should become foo-01.jpg
+ $this->assert_same(pathinfo($photo1->name, PATHINFO_FILENAME) . "-01.jpg", $photo2->name);
- // foo should become foo
- $this->assert_true(preg_match("/{$photo1->slug}/", $photo2->name));
+ // foo should become foo-01
+ $this->assert_same("{$photo1->slug}-01", $photo2->slug);
}
public function move_album_inside_descendent_fails_test() {
@@ -342,30 +323,17 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
}
public function photo_files_must_have_an_extension_test() {
- try {
- $photo = test::random_photo_unsaved();
- $photo->mime_type = "image/jpeg";
- $photo->name = "no_extension";
- $photo->save();
- } catch (ORM_Validation_Exception $e) {
- $this->assert_same(array("name" => "illegal_data_file_extension"), $e->validation->errors());
- return; // pass
- }
- $this->assert_true(false, "Shouldn't get here");
+ $photo = test::random_photo_unsaved();
+ $photo->name = "no_extension_photo";
+ $photo->save();
+ $this->assert_equal("no_extension_photo.jpg", $photo->name);
}
public function movie_files_must_have_an_extension_test() {
- try {
- $movie = test::random_movie_unsaved();
- $movie->type = "movie";
- $movie->mime_type = "video/x-flv";
- $movie->name = "no_extension";
- $movie->save();
- } catch (ORM_Validation_Exception $e) {
- $this->assert_same(array("name" => "illegal_data_file_extension"), $e->validation->errors());
- return; // pass
- }
- $this->assert_true(false, "Shouldn't get here");
+ $movie = test::random_movie_unsaved();
+ $movie->name = "no_extension_movie";
+ $movie->save();
+ $this->assert_equal("no_extension_movie.flv", $movie->name);
}
public function cant_delete_root_album_test() {
@@ -399,7 +367,16 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$this->assert_false($response["can_edit"]);
}
- public function first_photo_becomes_album_cover() {
+ public function as_restful_array_with_add_bit_test() {
+ $response = item::root()->as_restful_array();
+ $this->assert_true($response["can_add"]);
+
+ identity::set_active_user(identity::guest());
+ $response = item::root()->as_restful_array();
+ $this->assert_false($response["can_add"]);
+ }
+
+ public function first_photo_becomes_album_cover_test() {
$album = test::random_album();
$photo = test::random_photo($album);
$album->reload();
@@ -450,8 +427,21 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$photo->set_data_file(MODPATH . "gallery/tests/Item_Model_Test.php");
$photo->save();
} catch (ORM_Validation_Exception $e) {
- $this->assert_same(array("mime_type" => "invalid", "name" => "illegal_data_file_extension"),
- $e->validation->errors());
+ $this->assert_same(array("name" => "invalid_data_file"), $e->validation->errors());
+ return; // pass
+ }
+ $this->assert_true(false, "Shouldn't get here");
+ }
+
+ public function unsafe_data_file_replacement_with_valid_extension_test() {
+ $temp_file = TMPPATH . "masquerading_php.jpg";
+ copy(MODPATH . "gallery/tests/Item_Model_Test.php", $temp_file);
+ try {
+ $photo = test::random_photo();
+ $photo->set_data_file($temp_file);
+ $photo->save();
+ } catch (ORM_Validation_Exception $e) {
+ $this->assert_same(array("name" => "invalid_data_file"), $e->validation->errors());
return; // pass
}
$this->assert_true(false, "Shouldn't get here");
@@ -469,55 +459,72 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
preg_match("|http://./var/albums/name_\w+\.jpg\?m=\d+|", $photo->file_url()),
$photo->file_url() . " is malformed");
- // Albums have special thumbnails. Empty album has cachebuster of 0 since it has no thumbnail
$album = test::random_album();
$this->assert_true(
- preg_match("|http://./var/thumbs/name_\w+/\.album\.jpg\?m=0|", $album->thumb_url()),
+ preg_match("|http://./var/thumbs/name_\w+/\.album\.jpg\?m=\d+|", $album->thumb_url()),
$album->thumb_url() . " is malformed");
$photo = test::random_photo($album);
$this->assert_true(
preg_match("|http://./var/thumbs/name_\w+/\.album\.jpg\?m=\d+|", $album->thumb_url()),
$album->thumb_url() . " is malformed");
+
+ // If the file does not exist, we should return a cache buster of m=0.
+ unlink($album->thumb_path());
+ $this->assert_true(
+ preg_match("|http://./var/thumbs/name_\w+/\.album\.jpg\?m=0|", $album->thumb_url()),
+ $album->thumb_url() . " is malformed");
}
- public function legal_extension_test() {
- foreach (array("test.gif", "test.GIF", "test.Gif", "test.jpeg", "test.JPG") as $name) {
+ public function legal_extension_that_does_match_gets_used_test() {
+ foreach (array("jpg", "JPG", "Jpg", "jpeg") as $extension) {
$photo = test::random_photo_unsaved(item::root());
- $photo->name = $name;
+ $photo->name = test::random_name() . ".{$extension}";
$photo->save();
+ // Should get renamed with the correct jpg extension of the data file.
+ $this->assert_equal($extension, pathinfo($photo->name, PATHINFO_EXTENSION));
}
}
public function illegal_extension_test() {
foreach (array("test.php", "test.PHP", "test.php5", "test.php4",
"test.pl", "test.php.png") as $name) {
- try {
- $photo = test::random_photo_unsaved(item::root());
- $photo->name = $name;
- $photo->save();
- } catch (ORM_Validation_Exception $e) {
- $this->assert_equal(array("name" => "illegal_data_file_extension"),
- $e->validation->errors());
- continue;
- }
- $this->assert_true(false, "Shouldn't get here");
+ $photo = test::random_photo_unsaved(item::root());
+ $photo->name = $name;
+ $photo->save();
+ // Should get renamed with the correct jpg extension of the data file.
+ $this->assert_equal("jpg", pathinfo($photo->name, PATHINFO_EXTENSION));
}
}
public function cant_rename_to_illegal_extension_test() {
foreach (array("test.php.test", "test.php", "test.PHP",
"test.php5", "test.php4", "test.pl") as $name) {
- try {
- $photo = test::random_photo(item::root());
- $photo->name = $name;
- $photo->save();
- } catch (ORM_Validation_Exception $e) {
- $this->assert_equal(array("name" => "illegal_data_file_extension"),
- $e->validation->errors());
- continue;
- }
- $this->assert_true(false, "Shouldn't get here");
+ $photo = test::random_photo(item::root());
+ $photo->name = $name;
+ $photo->save();
+ // Should get renamed with the correct jpg extension of the data file.
+ $this->assert_equal("jpg", pathinfo($photo->name, PATHINFO_EXTENSION));
+ }
+ }
+
+ public function legal_extension_that_doesnt_match_gets_fixed_test() {
+ foreach (array("test.png", "test.mp4", "test.GIF") as $name) {
+ $photo = test::random_photo_unsaved(item::root());
+ $photo->name = $name;
+ $photo->save();
+ // Should get renamed with the correct jpg extension of the data file.
+ $this->assert_equal("jpg", pathinfo($photo->name, PATHINFO_EXTENSION));
+ }
+ }
+
+ public function rename_to_legal_extension_that_doesnt_match_gets_fixed_test() {
+ foreach (array("test.png", "test.mp4", "test.GIF") as $name) {
+ $photo = test::random_photo(item::root());
+ $photo->name = $name;
+ $photo->save();
+ // Should get renamed with the correct jpg extension of the data file.
+ $this->assert_equal("jpg", pathinfo($photo->name, PATHINFO_EXTENSION));
}
}
@@ -526,4 +533,164 @@ class Item_Model_Test extends Gallery_Unit_Test_Case {
$album->name = $album->name . ".foo.bar";
$album->save();
}
+
+ public function no_conflict_when_parents_different_test() {
+ $parent1 = test::random_album();
+ $parent2 = test::random_album();
+ $photo1 = test::random_photo($parent1);
+ $photo2 = test::random_photo($parent2);
+
+ $photo2->name = $photo1->name;
+ $photo2->slug = $photo1->slug;
+ $photo2->save();
+
+ // photo2 has same name and slug as photo1 but different parent - no conflict.
+ $this->assert_same($photo1->name, $photo2->name);
+ $this->assert_same($photo1->slug, $photo2->slug);
+ }
+
+ public function fix_conflict_when_names_identical_test() {
+ $parent = test::random_album();
+ $photo1 = test::random_photo($parent);
+ $photo2 = test::random_photo($parent);
+
+ $photo1_orig_base = pathinfo($photo1->name, PATHINFO_FILENAME);
+ $photo2_orig_slug = $photo2->slug;
+
+ $photo2->name = $photo1->name;
+ $photo2->save();
+
+ // photo2 has same name as photo1 - conflict resolved by renaming with -01.
+ $this->assert_same("{$photo1_orig_base}-01.jpg", $photo2->name);
+ $this->assert_same("{$photo2_orig_slug}-01", $photo2->slug);
+ }
+
+ public function fix_conflict_when_slugs_identical_test() {
+ $parent = test::random_album();
+ $photo1 = test::random_photo($parent);
+ $photo2 = test::random_photo($parent);
+
+ $photo2_orig_base = pathinfo($photo2->name, PATHINFO_FILENAME);
+
+ $photo2->slug = $photo1->slug;
+ $photo2->save();
+
+ // photo2 has same slug as photo1 - conflict resolved by renaming with -01.
+ $this->assert_same("{$photo2_orig_base}-01.jpg", $photo2->name);
+ $this->assert_same("{$photo1->slug}-01", $photo2->slug);
+ }
+
+ public function no_conflict_when_parents_different_for_albums_test() {
+ $parent1 = test::random_album();
+ $parent2 = test::random_album();
+ $album1 = test::random_album($parent1);
+ $album2 = test::random_album($parent2);
+
+ $album2->name = $album1->name;
+ $album2->slug = $album1->slug;
+ $album2->save();
+
+ // album2 has same name and slug as album1 but different parent - no conflict.
+ $this->assert_same($album1->name, $album2->name);
+ $this->assert_same($album1->slug, $album2->slug);
+ }
+
+ public function fix_conflict_when_names_identical_for_albums_test() {
+ $parent = test::random_album();
+ $album1 = test::random_album($parent);
+ $album2 = test::random_album($parent);
+
+ $album2_orig_slug = $album2->slug;
+
+ $album2->name = $album1->name;
+ $album2->save();
+
+ // album2 has same name as album1 - conflict resolved by renaming with -01.
+ $this->assert_same("{$album1->name}-01", $album2->name);
+ $this->assert_same("{$album2_orig_slug}-01", $album2->slug);
+ }
+
+ public function fix_conflict_when_slugs_identical_for_albums_test() {
+ $parent = test::random_album();
+ $album1 = test::random_album($parent);
+ $album2 = test::random_album($parent);
+
+ $album2_orig_name = $album2->name;
+
+ $album2->slug = $album1->slug;
+ $album2->save();
+
+ // album2 has same slug as album1 - conflict resolved by renaming with -01.
+ $this->assert_same("{$album2_orig_name}-01", $album2->name);
+ $this->assert_same("{$album1->slug}-01", $album2->slug);
+ }
+
+ public function no_conflict_when_base_names_identical_between_album_and_photo_test() {
+ $parent = test::random_album();
+ $album = test::random_album($parent);
+ $photo = test::random_photo($parent);
+
+ $photo_orig_slug = $photo->slug;
+
+ $photo->name = "{$album->name}.jpg";
+ $photo->save();
+
+ // photo has same base name as album - no conflict.
+ $this->assert_same("{$album->name}.jpg", $photo->name);
+ $this->assert_same($photo_orig_slug, $photo->slug);
+ }
+
+ public function fix_conflict_when_full_names_identical_between_album_and_photo_test() {
+ $parent = test::random_album();
+ $photo = test::random_photo($parent);
+ $album = test::random_album($parent);
+
+ $album_orig_slug = $album->slug;
+
+ $album->name = $photo->name;
+ $album->save();
+
+ // album has same full name as album - conflict resolved by renaming with -01.
+ $this->assert_same("{$photo->name}-01", $album->name);
+ $this->assert_same("{$album_orig_slug}-01", $album->slug);
+ }
+
+ public function fix_conflict_when_slugs_identical_between_album_and_photo_test() {
+ $parent = test::random_album();
+ $album = test::random_album($parent);
+ $photo = test::random_photo($parent);
+
+ $photo_orig_base = pathinfo($photo->name, PATHINFO_FILENAME);
+
+ $photo->slug = $album->slug;
+ $photo->save();
+
+ // photo has same slug as album - conflict resolved by renaming with -01.
+ $this->assert_same("{$photo_orig_base}-01.jpg", $photo->name);
+ $this->assert_same("{$album->slug}-01", $photo->slug);
+ }
+
+ public function fix_conflict_when_base_names_identical_between_jpg_png_flv_test() {
+ $parent = test::random_album();
+ $item1 = test::random_photo($parent);
+ $item2 = test::random_photo($parent);
+ $item3 = test::random_movie($parent);
+
+ $item1_orig_base = pathinfo($item1->name, PATHINFO_FILENAME);
+ $item2_orig_slug = $item2->slug;
+ $item3_orig_slug = $item3->slug;
+
+ $item2->set_data_file(MODPATH . "gallery/images/graphicsmagick.png");
+ $item2->name = "{$item1_orig_base}.png";
+ $item2->save();
+
+ $item3->name = "{$item1_orig_base}.flv";
+ $item3->save();
+
+ // item2 and item3 have same base name as item1 - conflict resolved by renaming with -01 and -02.
+ $this->assert_same("{$item1_orig_base}-01.png", $item2->name);
+ $this->assert_same("{$item2_orig_slug}-01", $item2->slug);
+ $this->assert_same("{$item1_orig_base}-02.flv", $item3->name);
+ $this->assert_same("{$item3_orig_slug}-02", $item3->slug);
+ }
}
diff --git a/modules/gallery/tests/Legal_File_Helper_Test.php b/modules/gallery/tests/Legal_File_Helper_Test.php
index 5db99935..7ed5214b 100644
--- a/modules/gallery/tests/Legal_File_Helper_Test.php
+++ b/modules/gallery/tests/Legal_File_Helper_Test.php
@@ -40,6 +40,63 @@ class Legal_File_Helper_Test extends Gallery_Unit_Test_Case {
$this->assert_equal(3, count(legal_file::get_movie_types_by_extension()));
}
+ public function get_types_by_extension_test() {
+ $this->assert_equal("image/jpeg", legal_file::get_types_by_extension("jpg")); // photo
+ $this->assert_equal("video/x-flv", legal_file::get_types_by_extension("FLV")); // movie
+ $this->assert_equal(null, legal_file::get_types_by_extension("php")); // invalid
+ $this->assert_equal(null, legal_file::get_types_by_extension("php.flv")); // invalid w/ .
+
+ // No extension returns full array
+ $this->assert_equal(7, count(legal_file::get_types_by_extension()));
+ }
+
+ public function get_photo_extensions_test() {
+ $this->assert_equal(true, legal_file::get_photo_extensions("jpg")); // regular
+ $this->assert_equal(true, legal_file::get_photo_extensions("JPG")); // all caps
+ $this->assert_equal(true, legal_file::get_photo_extensions("Png")); // some caps
+ $this->assert_equal(false, legal_file::get_photo_extensions("php")); // invalid
+ $this->assert_equal(false, legal_file::get_photo_extensions("php.jpg")); // invalid w/ .
+
+ // No extension returns full array
+ $this->assert_equal(4, count(legal_file::get_photo_extensions()));
+ }
+
+ public function get_movie_extensions_test() {
+ $this->assert_equal(true, legal_file::get_movie_extensions("flv")); // regular
+ $this->assert_equal(true, legal_file::get_movie_extensions("FLV")); // all caps
+ $this->assert_equal(true, legal_file::get_movie_extensions("Mp4")); // some caps
+ $this->assert_equal(false, legal_file::get_movie_extensions("php")); // invalid
+ $this->assert_equal(false, legal_file::get_movie_extensions("php.jpg")); // invalid w/ .
+
+ // No extension returns full array
+ $this->assert_equal(3, count(legal_file::get_movie_extensions()));
+ }
+
+ public function get_extensions_test() {
+ $this->assert_equal(true, legal_file::get_extensions("jpg")); // photo
+ $this->assert_equal(true, legal_file::get_extensions("FLV")); // movie
+ $this->assert_equal(false, legal_file::get_extensions("php")); // invalid
+ $this->assert_equal(false, legal_file::get_extensions("php.jpg")); // invalid w/ .
+
+ // No extension returns full array
+ $this->assert_equal(7, count(legal_file::get_extensions()));
+ }
+
+ public function get_filters_test() {
+ // All 7 extensions both uppercase and lowercase
+ $this->assert_equal(14, count(legal_file::get_filters()));
+ }
+
+ public function get_photo_types_test() {
+ // Note that this is one *less* than photo extensions since jpeg and jpg have the same mime.
+ $this->assert_equal(3, count(legal_file::get_photo_types()));
+ }
+
+ public function get_movie_types_test() {
+ // Note that this is one *more* than movie extensions since video/flv is added.
+ $this->assert_equal(4, count(legal_file::get_movie_types()));
+ }
+
public function change_extension_test() {
$this->assert_equal("foo.jpg", legal_file::change_extension("foo.png", "jpg"));
}
@@ -79,10 +136,62 @@ class Legal_File_Helper_Test extends Gallery_Unit_Test_Case {
public function smash_extensions_test() {
$this->assert_equal("foo_bar.jpg", legal_file::smash_extensions("foo.bar.jpg"));
$this->assert_equal("foo_bar_baz.jpg", legal_file::smash_extensions("foo.bar.baz.jpg"));
- $this->assert_equal("foo_bar_baz.jpg", legal_file::smash_extensions("foo.bar.baz.jpg"));
$this->assert_equal("foo_bar_baz.jpg", legal_file::smash_extensions("...foo...bar..baz...jpg"));
$this->assert_equal("/path/to/foo_bar.jpg", legal_file::smash_extensions("/path/to/foo.bar.jpg"));
$this->assert_equal("/path/to.to/foo_bar.jpg", legal_file::smash_extensions("/path/to.to/foo.bar.jpg"));
$this->assert_equal("foo_bar-12345678.jpg", legal_file::smash_extensions("foo.bar-12345678.jpg"));
}
+
+ public function smash_extensions_pass_thru_names_without_extensions_test() {
+ $this->assert_equal("foo", legal_file::smash_extensions("foo"));
+ $this->assert_equal("foo.", legal_file::smash_extensions("foo."));
+ $this->assert_equal(".foo", legal_file::smash_extensions(".foo"));
+ $this->assert_equal(".", legal_file::smash_extensions("."));
+ $this->assert_equal("", legal_file::smash_extensions(""));
+ $this->assert_equal(null, legal_file::smash_extensions(null));
+ }
+
+ public function sanitize_filename_with_no_rename_test() {
+ $this->assert_equal("foo.jpeg", legal_file::sanitize_filename("foo.jpeg", "jpg", "photo"));
+ $this->assert_equal("foo.jpg", legal_file::sanitize_filename("foo.jpg", "jpeg", "photo"));
+ $this->assert_equal("foo.MP4", legal_file::sanitize_filename("foo.MP4", "mp4", "movie"));
+ $this->assert_equal("foo.mp4", legal_file::sanitize_filename("foo.mp4", "MP4", "movie"));
+ }
+
+ public function sanitize_filename_with_corrected_extension_test() {
+ $this->assert_equal("foo.jpg", legal_file::sanitize_filename("foo.png", "jpg", "photo"));
+ $this->assert_equal("foo.MP4", legal_file::sanitize_filename("foo.jpg", "MP4", "movie"));
+ $this->assert_equal("foo.jpg", legal_file::sanitize_filename("foo.php", "jpg", "photo"));
+ }
+
+ public function sanitize_filename_with_non_standard_chars_and_dots_test() {
+ $this->assert_equal("foo.jpg", legal_file::sanitize_filename("foo", "jpg", "photo"));
+ $this->assert_equal("foo.mp4", legal_file::sanitize_filename("foo.", "mp4", "movie"));
+ $this->assert_equal("foo.jpeg", legal_file::sanitize_filename(".foo.jpeg", "jpg", "photo"));
+ $this->assert_equal("foo_2013_02_10.jpeg",
+ legal_file::sanitize_filename("foo.2013/02/10.jpeg", "jpg", "photo"));
+ $this->assert_equal("foo_bar_baz.jpg",
+ legal_file::sanitize_filename("...foo...bar..baz...png", "jpg", "photo"));
+ $this->assert_equal("j'écris@un#nom_bizarre(mais quand_même_ça_passe.jpg",
+ legal_file::sanitize_filename("/j'écris@un#nom/bizarre(mais quand.même/ça_passe.\$ÇÀ@€#_", "jpg", "photo"));
+ }
+
+ public function sanitize_filename_with_no_base_name_test() {
+ $this->assert_equal("photo.jpg", legal_file::sanitize_filename(".png", "jpg", "photo"));
+ $this->assert_equal("movie.mp4", legal_file::sanitize_filename("__..__", "mp4", "movie"));
+ $this->assert_equal("photo.jpg", legal_file::sanitize_filename(".", "jpg", "photo"));
+ $this->assert_equal("movie.mp4", legal_file::sanitize_filename(null, "mp4", "movie"));
+ }
+
+ public function sanitize_filename_with_invalid_arguments_test() {
+ foreach (array("flv" => "photo", "jpg" => "movie", "php" => "photo",
+ null => "movie", "jpg" => "album", "jpg" => null) as $extension => $type) {
+ try {
+ legal_file::sanitize_filename("foo.jpg", $extension, $type);
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/modules/gallery/tests/Movie_Helper_Test.php b/modules/gallery/tests/Movie_Helper_Test.php
index ff7f798c..03fa2da9 100644
--- a/modules/gallery/tests/Movie_Helper_Test.php
+++ b/modules/gallery/tests/Movie_Helper_Test.php
@@ -46,4 +46,60 @@ class Movie_Helper_Test extends Gallery_Unit_Test_Case {
$this->assert_equal($seconds, movie::hhmmssdd_to_seconds($hhmmssdd));
}
}
+
+ public function get_file_metadata_test() {
+ $movie = test::random_movie();
+ $this->assert_equal(array(360, 288, "video/x-flv", "flv", 6.00),
+ movie::get_file_metadata($movie->file_path()));
+ }
+
+ public function get_file_metadata_with_non_existent_file_test() {
+ try {
+ $metadata = movie::get_file_metadata(MODPATH . "gallery/tests/this_does_not_exist");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_no_extension_test() {
+ copy(MODPATH . "gallery/tests/test.flv", TMPPATH . "test_flv_with_no_extension");
+ // Since mime type and extension are based solely on the filename, this is considered invalid.
+ try {
+ $metadata = movie::get_file_metadata(TMPPATH . "test_flv_with_no_extension");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_illegal_extension_test() {
+ try {
+ $metadata = movie::get_file_metadata(MODPATH . "gallery/tests/Movie_Helper_Test.php");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_illegal_extension_but_valid_file_contents_test() {
+ copy(MODPATH . "gallery/tests/test.flv", TMPPATH . "test_flv_with_php_extension.php");
+ // Since mime type and extension are based solely on the filename, this is considered invalid.
+ try {
+ $metadata = movie::get_file_metadata(TMPPATH . "test_flv_with_php_extension.php");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_valid_extension_but_illegal_file_contents_test() {
+ copy(MODPATH . "gallery/tests/Photo_Helper_Test.php", TMPPATH . "test_php_with_flv_extension.flv");
+ // Since mime type and extension are based solely on the filename, this is considered valid.
+ // Of course, FFmpeg cannot extract width, height, or duration from the file. Note that this
+ // isn't a really a security problem, since the filename doesn't have a php extension and
+ // therefore will never be executed.
+ $this->assert_equal(array(0, 0, "video/x-flv", "flv", 0),
+ movie::get_file_metadata(TMPPATH . "test_php_with_flv_extension.flv"));
+ }
}
diff --git a/modules/gallery/tests/Photo_Helper_Test.php b/modules/gallery/tests/Photo_Helper_Test.php
new file mode 100644
index 00000000..79b5ccfd
--- /dev/null
+++ b/modules/gallery/tests/Photo_Helper_Test.php
@@ -0,0 +1,70 @@
+<?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 Photo_Helper_Test extends Gallery_Unit_Test_Case {
+ public function get_file_metadata_test() {
+ $photo = test::random_photo();
+ $this->assert_equal(array(1024, 768, "image/jpeg", "jpg"),
+ photo::get_file_metadata($photo->file_path()));
+ }
+
+ public function get_file_metadata_with_non_existent_file_test() {
+ try {
+ $metadata = photo::get_file_metadata(MODPATH . "gallery/tests/this_does_not_exist");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_no_extension_test() {
+ copy(MODPATH . "gallery/tests/test.jpg", TMPPATH . "test_jpg_with_no_extension");
+ $this->assert_equal(array(1024, 768, "image/jpeg", "jpg"),
+ photo::get_file_metadata(TMPPATH . "test_jpg_with_no_extension"));
+ }
+
+ public function get_file_metadata_with_illegal_extension_test() {
+ try {
+ $metadata = photo::get_file_metadata(MODPATH . "gallery/tests/Photo_Helper_Test.php");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+
+ public function get_file_metadata_with_illegal_extension_but_valid_file_contents_test() {
+ // This ensures that we correctly "re-type" files with invalid extensions if the contents
+ // themselves are valid. This is needed to ensure that issues similar to those corrected by
+ // ticket #1855, where an image that looked valid (header said jpg) with a php extension was
+ // previously accepted without changing its extension, do not arise and cause security issues.
+ copy(MODPATH . "gallery/tests/test.jpg", TMPPATH . "test_jpg_with_php_extension.php");
+ $this->assert_equal(array(1024, 768, "image/jpeg", "jpg"),
+ photo::get_file_metadata(TMPPATH . "test_jpg_with_php_extension.php"));
+ }
+
+ public function get_file_metadata_with_valid_extension_but_illegal_file_contents_test() {
+ copy(MODPATH . "gallery/tests/Photo_Helper_Test.php", TMPPATH . "test_php_with_jpg_extension.jpg");
+ try {
+ $metadata = photo::get_file_metadata(TMPPATH . "test_php_with_jpg_extension.jpg");
+ $this->assert_true(false, "Shouldn't get here");
+ } catch (Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/modules/gallery/tests/controller_auth_data.txt b/modules/gallery/tests/controller_auth_data.txt
index a7bc28dd..9473f9f6 100644
--- a/modules/gallery/tests/controller_auth_data.txt
+++ b/modules/gallery/tests/controller_auth_data.txt
@@ -25,6 +25,7 @@ modules/gallery/controllers/user_profile.php send
modules/gallery/controllers/welcome_message.php index DIRTY_AUTH
modules/organize/controllers/organize.php tree DIRTY_CSRF
modules/organize/controllers/organize.php delete DIRTY_AUTH
+modules/organize/controllers/organize.php tag DIRTY_AUTH
modules/rest/controllers/rest.php index DIRTY_CSRF|DIRTY_AUTH
modules/rest/controllers/rest.php reset_api_key_confirm DIRTY_AUTH
modules/rest/controllers/rest.php reset_api_key DIRTY_AUTH
diff --git a/modules/gallery/tests/xss_data.txt b/modules/gallery/tests/xss_data.txt
index 7d77645d..51347f86 100644
--- a/modules/gallery/tests/xss_data.txt
+++ b/modules/gallery/tests/xss_data.txt
@@ -295,19 +295,21 @@ modules/organize/views/organize_frame.html.php 12 DIRTY_JS url::f
modules/organize/views/organize_frame.html.php 56 DIRTY_JS url::site("organize/album_info/__ID__")
modules/organize/views/organize_frame.html.php 94 DIRTY_JS access::csrf_token()
modules/organize/views/organize_frame.html.php 96 DIRTY_JS url::site("organize/set_sort/__ID__")
-modules/organize/views/organize_frame.html.php 116 DIRTY_JS url::site("organize/delete")
-modules/organize/views/organize_frame.html.php 125 DIRTY_JS access::csrf_token()
-modules/organize/views/organize_frame.html.php 238 DIRTY_JS url::site("organize/rearrange")
-modules/organize/views/organize_frame.html.php 249 DIRTY_JS access::csrf_token()
-modules/organize/views/organize_frame.html.php 287 DIRTY_JS $key
-modules/organize/views/organize_frame.html.php 410 DIRTY_JS url::site("organize/tree/{$album->id}")
-modules/organize/views/organize_frame.html.php 468 DIRTY_JS url::site("organize/reparent")
-modules/organize/views/organize_frame.html.php 491 DIRTY_JS access::csrf_token()
-modules/organize/views/organize_frame.html.php 507 DIRTY_JS access::can("edit",item::root())
-modules/organize/views/organize_frame.html.php 509 DIRTY_JS html::clean(item::root()->title)
-modules/organize/views/organize_frame.html.php 511 DIRTY_JS item::root()->id
-modules/organize/views/organize_frame.html.php 519 DIRTY_JS $album->id
-modules/organize/views/organize_frame.html.php 520 DIRTY_JS $album->id
+modules/organize/views/organize_frame.html.php 116 DIRTY_JS url::site("organize/tag")
+modules/organize/views/organize_frame.html.php 126 DIRTY_JS access::csrf_token()
+modules/organize/views/organize_frame.html.php 140 DIRTY_JS url::site("organize/delete")
+modules/organize/views/organize_frame.html.php 149 DIRTY_JS access::csrf_token()
+modules/organize/views/organize_frame.html.php 264 DIRTY_JS url::site("organize/rearrange")
+modules/organize/views/organize_frame.html.php 275 DIRTY_JS access::csrf_token()
+modules/organize/views/organize_frame.html.php 314 DIRTY_JS $key
+modules/organize/views/organize_frame.html.php 476 DIRTY_JS url::site("organize/tree/{$album->id}")
+modules/organize/views/organize_frame.html.php 534 DIRTY_JS url::site("organize/reparent")
+modules/organize/views/organize_frame.html.php 557 DIRTY_JS access::csrf_token()
+modules/organize/views/organize_frame.html.php 573 DIRTY_JS access::can("edit",item::root())
+modules/organize/views/organize_frame.html.php 575 DIRTY_JS html::clean(item::root()->title)
+modules/organize/views/organize_frame.html.php 577 DIRTY_JS item::root()->id
+modules/organize/views/organize_frame.html.php 585 DIRTY_JS $album->id
+modules/organize/views/organize_frame.html.php 586 DIRTY_JS $album->id
modules/recaptcha/views/admin_recaptcha.html.php 11 DIRTY $form
modules/recaptcha/views/admin_recaptcha.html.php 23 DIRTY_JS $public_key
modules/recaptcha/views/form_recaptcha.html.php 3 DIRTY_ATTR request::protocol()
diff --git a/modules/gallery/views/movieplayer.html.php b/modules/gallery/views/movieplayer.html.php
index 25cb9f58..edb5184c 100644
--- a/modules/gallery/views/movieplayer.html.php
+++ b/modules/gallery/views/movieplayer.html.php
@@ -18,7 +18,7 @@
$("#" + id).css({width: width, height: height});
};
// setup flowplayer
- flowplayer(id,
+ flowplayer(id,
$.extend(true, {
"src": "<?= url::abs_file("lib/flowplayer.swf") ?>",
"wmode": "transparent",
diff --git a/modules/gallery_unit_test/controllers/gallery_unit_test.php b/modules/gallery_unit_test/controllers/gallery_unit_test.php
index df4b1f4a..67d006b3 100644
--- a/modules/gallery_unit_test/controllers/gallery_unit_test.php
+++ b/modules/gallery_unit_test/controllers/gallery_unit_test.php
@@ -108,7 +108,7 @@ class Gallery_Unit_Test_Controller extends Controller {
// Install the active modules
// Force gallery and user to be installed first to resolve dependencies.
- gallery_installer::install(true);
+ module::install("gallery");
module::load_modules();
module::install("user");
diff --git a/modules/gallery_unit_test/helpers/test.php b/modules/gallery_unit_test/helpers/test.php
index 3ccb35ca..75cf9b33 100644
--- a/modules/gallery_unit_test/helpers/test.php
+++ b/modules/gallery_unit_test/helpers/test.php
@@ -63,6 +63,34 @@ class test_Core {
return test::random_photo_unsaved($parent)->save()->reload();
}
+ // If a test compares photo file contents (i.e. file_get_contents), it's best to use this
+ // function to guarantee uniqueness.
+ static function random_unique_photo_unsaved($parent=null) {
+ $rand = test::random_string(6);
+ $photo = ORM::factory("item");
+ $photo->type = "photo";
+ $photo->parent_id = $parent ? $parent->id : 1;
+ if (function_exists("gd_info")) {
+ // Make image unique - color the black dot of test.jpg to the 6-digit hex code of rand.
+ $image = imagecreatefromjpeg(MODPATH . "gallery/tests/test.jpg");
+ imagefilter($image, IMG_FILTER_COLORIZE,
+ hexdec(substr($rand, 0, 2)), hexdec(substr($rand, 2, 2)), hexdec(substr($rand, 4, 2)));
+ imagejpeg($image, TMPPATH . "test_$rand.jpg");
+ imagedestroy($image);
+ $photo->set_data_file(TMPPATH . "test_$rand.jpg");
+ } else {
+ // Just use the black dot.
+ $photo->set_data_file(MODPATH . "gallery/tests/test.jpg");
+ }
+ $photo->name = "name_$rand.jpg";
+ $photo->title = "title_$rand";
+ return $photo;
+ }
+
+ static function random_unique_photo($parent=null) {
+ return test::random_unique_photo_unsaved($parent)->save()->reload();
+ }
+
static function random_user($password="password") {
$rand = "name_" . test::random_string(6);
return identity::create_user($rand, $rand, $password, "$rand@rand.com");
diff --git a/modules/image_block/helpers/image_block_installer.php b/modules/image_block/helpers/image_block_installer.php
index 8558fe51..b177b971 100644
--- a/modules/image_block/helpers/image_block_installer.php
+++ b/modules/image_block/helpers/image_block_installer.php
@@ -21,7 +21,6 @@ class image_block_installer {
static function install() {
module::set_var("image_block", "image_count", "1");
- module::set_version("image_block", $version = 3);
}
static function upgrade($version) {
diff --git a/modules/info/helpers/info_block.php b/modules/info/helpers/info_block.php
index 4079fe55..62aa0746 100644
--- a/modules/info/helpers/info_block.php
+++ b/modules/info/helpers/info_block.php
@@ -29,7 +29,7 @@ class info_block_Core {
if ($theme->item()) {
$block = new Block();
$block->css_id = "g-metadata";
- $block->title = $theme->item()->is_album() ? t("Album info") :
+ $block->title = $theme->item()->is_album() ? t("Album info") :
($theme->item()->is_movie() ? t("Movie info") : t("Photo info"));
$block->content = new View("info_block.html");
if ($theme->item->title && module::get_var("info", "show_title")) {
diff --git a/modules/info/helpers/info_installer.php b/modules/info/helpers/info_installer.php
index 560af15c..43c216dc 100644
--- a/modules/info/helpers/info_installer.php
+++ b/modules/info/helpers/info_installer.php
@@ -25,7 +25,6 @@ class info_installer {
module::set_var("info", "show_owner", 1);
module::set_var("info", "show_name", 1);
module::set_var("info", "show_captured", 1);
- module::set_version("info", 2);
}
static function upgrade($version) {
diff --git a/modules/notification/helpers/notification_installer.php b/modules/notification/helpers/notification_installer.php
index 58435641..f6b05c18 100644
--- a/modules/notification/helpers/notification_installer.php
+++ b/modules/notification/helpers/notification_installer.php
@@ -36,8 +36,6 @@ class notification_installer {
`text` text,
PRIMARY KEY (`id`))
DEFAULT CHARSET=utf8;");
-
- module::set_version("notification", 2);
}
static function upgrade($version) {
diff --git a/modules/organize/controllers/organize.php b/modules/organize/controllers/organize.php
index f090b8a9..ba73ae75 100644
--- a/modules/organize/controllers/organize.php
+++ b/modules/organize/controllers/organize.php
@@ -129,12 +129,8 @@ class Organize_Controller extends Controller {
if ($album->sort_column != "weight") {
// Force all the weights into the current order before changing the order to manual
- $weight = 0;
- foreach ($album->children() as $child) {
- $child->weight = ++$weight;
- $child->save();
- }
-
+ // @todo: consider making this a trigger in the Item_Model.
+ item::resequence_child_weights($album);
$album->sort_column = "weight";
$album->sort_order = "ASC";
$album->save();
@@ -158,7 +154,7 @@ class Organize_Controller extends Controller {
// Move all the source items to the right spots.
for ($i = 0; $i < count($source_ids); $i++) {
$source = ORM::factory("item", $source_ids[$i]);
- if ($source->parent_id = $album->id) {
+ if ($source->parent_id == $album->id) {
$source->weight = $base_weight + $i;
$source->save();
}
@@ -182,6 +178,28 @@ class Organize_Controller extends Controller {
json::reply(null);
}
+ function tag() {
+ access::verify_csrf();
+ $input = Input::instance();
+
+ foreach (explode(",", $input->post("item_ids")) as $item_id) {
+ $item = ORM::factory("item", $item_id);
+ if (access::can("edit", $item)) {
+ // Assuming the user can view/edit the current item, loop
+ // through each tag that was submitted and apply it to
+ // the current item.
+ foreach (explode(",", $input->post("tag_names")) as $tag_name) {
+ $tag_name = trim($tag_name);
+ if ($tag_name) {
+ tag::add($item, $tag_name);
+ }
+ }
+ }
+ }
+
+ json::reply(null);
+ }
+
private function _get_tree($item, $selected) {
$tree = array();
$children = $item->viewable()
diff --git a/modules/organize/views/organize_frame.html.php b/modules/organize/views/organize_frame.html.php
index 51d49104..3a70ccff 100644
--- a/modules/organize/views/organize_frame.html.php
+++ b/modules/organize/views/organize_frame.html.php
@@ -104,9 +104,33 @@
});
}
+ var tag_selected_items = function(tag) {
+ var nodes = thumb_data_view.getSelectedNodes();
+ var item_ids = [];
+ for (var i = 0; i != nodes.length; i++) {
+ var node = Ext.fly(nodes[i]);
+ item_ids.push(get_id_from_node(node));
+ }
+ start_busy(<?= t("Tagging...")->for_js() ?>);
+ Ext.Ajax.request({
+ url: '<?= url::site("organize/tag") ?>',
+ method: "post",
+ success: function() {
+ stop_busy();
+ reload_album_data();
+ },
+ failure: show_generic_error,
+ params: {
+ item_ids: item_ids.join(","),
+ tag_names: tag,
+ csrf: '<?= access::csrf_token() ?>'
+ }
+ });
+ };
+
var delete_selected_items = function() {
var nodes = thumb_data_view.getSelectedNodes();
- item_ids = [];
+ var item_ids = [];
for (var i = 0; i != nodes.length; i++) {
var node = Ext.fly(nodes[i]);
item_ids.push(get_id_from_node(node));
@@ -166,8 +190,10 @@
getDragData: function(e) {
var target = e.getTarget(v.itemSelector, 10);
if (target) {
- if (!v.isSelected(target)) {
- v.onClick(e);
+ if (e.ctrlKey == false) {
+ if (!v.isSelected(target)) {
+ v.onClick(e);
+ }
}
var selected_nodes = v.getSelectedNodes();
var drag_data = {
@@ -254,6 +280,7 @@
});
},
"selectionchange": function(v, selections) {
+ tag_button.setDisabled(!selections.length || !current_album_editable || !tag_textfield.getValue());
delete_button.setDisabled(!selections.length || !current_album_editable);
}
},
@@ -330,6 +357,31 @@
displayField: "value"
});
+ var tag_textfield = new Ext.form.TextField({
+ flex: 4,
+ enableKeyEvents: true,
+ listeners: {
+ "keyup": function(v, e) {
+ var nodes = thumb_data_view.getSelectedNodes();
+ tag_button.setDisabled(!nodes.length || !current_album_editable || !tag_textfield.getValue());
+ }
+ }
+ });
+
+ var tag_button = new Ext.Button({
+ flex: 2,
+ text: <?= t("Tag")->for_js() ?>,
+ cls: "x-btn-text-icon",
+ id: "tag-button",
+ disabled: true,
+ listeners: {
+ "click": function() {
+ tag_selected_items(tag_textfield.getValue());
+ return true;
+ }
+ }
+ });
+
var delete_button = new Ext.Button({
flex: 2,
text: <?= t("Delete")->for_js() ?>,
@@ -375,10 +427,24 @@
sort_column_combobox,
sort_order_combobox
]
- }, {
+ },
+<? if (module::is_active("tag")): ?>
+ {
+ xtype: "spacer",
+ flex: 3
+ },
+ tag_textfield,
+ tag_button,
+ {
+ xtype: "spacer",
+ flex: 1
+ },
+<? else: ?>
+ {
xtype: "spacer",
flex: 10
},
+<? endif ?>
delete_button,
{
xtype: "button",
diff --git a/modules/rest/helpers/rest.php b/modules/rest/helpers/rest.php
index eeedff2a..9b367feb 100644
--- a/modules/rest/helpers/rest.php
+++ b/modules/rest/helpers/rest.php
@@ -54,7 +54,7 @@ class rest_Core {
$html = t("Empty response");
}
print "<pre>$html</pre>";
- if (Session::instance()->get("profiler", false)) {
+ if (gallery::show_profiler()) {
Profiler::enable();
$profiler = new Profiler();
$profiler->render();
diff --git a/modules/rest/helpers/rest_installer.php b/modules/rest/helpers/rest_installer.php
index df7484fe..96f8acfa 100644
--- a/modules/rest/helpers/rest_installer.php
+++ b/modules/rest/helpers/rest_installer.php
@@ -29,7 +29,6 @@ class rest_installer {
UNIQUE KEY(`user_id`))
DEFAULT CHARSET=utf8;");
module::set_var("rest", "allow_guest_access", false);
- module::set_version("rest", 3);
}
static function upgrade($version) {
diff --git a/modules/search/controllers/search.php b/modules/search/controllers/search.php
index f7f1d981..25ccd81c 100644
--- a/modules/search/controllers/search.php
+++ b/modules/search/controllers/search.php
@@ -54,8 +54,6 @@ class Search_Controller extends Controller {
list ($count, $result) =
search::search_within_album($q_with_more_terms, $album, $page_size, $offset);
- $title = t("Search: %q", array("q" => $q_with_more_terms));
-
$max_pages = max(ceil($count / $page_size), 1);
$template = new Theme_View("page.html", "collection", "search");
@@ -77,28 +75,27 @@ class Search_Controller extends Controller {
print $template;
- item::set_display_context_callback(
- "Search_Controller::get_display_context", $album->id, $title, $q_with_more_terms, $q);
+ item::set_display_context_callback("Search_Controller::get_display_context", $album, $q);
}
- static function get_display_context($item, $album_id, $title, $query_terms, $q) {
- $album = ORM::factory("item", $album_id);
- $position = search::get_position_within_album($item, $query_terms, $album);
+ static function get_display_context($item, $album, $q) {
+ $q_with_more_terms = search::add_query_terms($q);
+ $position = search::get_position_within_album($item, $q_with_more_terms, $album);
if ($position > 1) {
list ($count, $result_data) =
- search::search_within_album($query_terms, $album, 3, $position - 2);
+ search::search_within_album($q_with_more_terms, $album, 3, $position - 2);
list ($previous_item, $ignore, $next_item) = $result_data;
} else {
$previous_item = null;
list ($count, $result_data) =
- search::search_within_album($query_terms, $album, 1, $position);
+ search::search_within_album($q_with_more_terms, $album, 1, $position);
list ($next_item) = $result_data;
}
$search_url = url::abs_site("search" .
"?q=" . urlencode($q) .
- "&album=" . urlencode($album_id) .
+ "&album=" . urlencode($album->id) .
"&show={$item->id}");
$root = item::root();
@@ -106,9 +103,15 @@ class Search_Controller extends Controller {
"previous_item" => $previous_item,
"next_item" => $next_item,
"sibling_count" => $count,
+ "siblings_callback" => array("Search_Controller::get_siblings", array($q, $album)),
"breadcrumbs" => array(
- Breadcrumb::instance($root->title, "/", $root->id)->set_first(),
+ Breadcrumb::instance($root->title, $root->url())->set_first(),
Breadcrumb::instance(t("Search: %q", array("q" => $q)), $search_url),
Breadcrumb::instance($item->title, $item->url())->set_last()));
}
+
+ static function get_siblings($q, $album, $limit=1000, $offset=1) {
+ $result = search::search_within_album(search::add_query_terms($q), $album, $limit, $offset);
+ return $result[1];
+ }
}
diff --git a/modules/search/helpers/search.php b/modules/search/helpers/search.php
index b1bde359..b21e59dd 100644
--- a/modules/search/helpers/search.php
+++ b/modules/search/helpers/search.php
@@ -132,11 +132,12 @@ class search_Core {
static function get_position_within_album($item, $q, $album) {
$page_size = module::get_var("gallery", "page_size", 9);
- $query = self::_build_query_base($q, $album, array("{items}.id = " . $item->id));
+ $query = self::_build_query_base($q, $album, array("{items}.id = " . $item->id)) .
+ "ORDER BY `score` DESC ";
$db = Database::instance();
// Truncate the score by two decimal places as this resolves the issues
- // that arise due to in exact numeric conversions.
+ // that arise due to inexact numeric conversions.
$current = $db->query($query)->current();
if (!$current) {
// We can't find this result in our result set - perhaps we've fallen out of context? Clear
@@ -149,7 +150,10 @@ class search_Core {
$score = substr($score, 0, strlen($score) - 2);
}
- $data = $db->query(self::_build_query_base($q, $album) . " HAVING `score` >= " . $score);
+ // Redo the query but only look for results greater than or equal to our current location
+ // then seek backwards until we find our item.
+ $data = $db->query(self::_build_query_base($q, $album) . " HAVING `score` >= " . $score .
+ "ORDER BY `score` DESC ");
$data->seek($data->count() - 1);
while ($data->get("id") != $item->id && $data->prev()->valid()) {
diff --git a/modules/search/helpers/search_installer.php b/modules/search/helpers/search_installer.php
index 78dbce38..c9e8f26c 100644
--- a/modules/search/helpers/search_installer.php
+++ b/modules/search/helpers/search_installer.php
@@ -30,7 +30,6 @@ class search_installer {
FULLTEXT INDEX (`data`))
ENGINE=MyISAM
DEFAULT CHARSET=utf8;");
- module::set_version("search", 1);
}
static function activate() {
diff --git a/modules/server_add/controllers/server_add.php b/modules/server_add/controllers/server_add.php
index df55d8db..f6e0a944 100644
--- a/modules/server_add/controllers/server_add.php
+++ b/modules/server_add/controllers/server_add.php
@@ -61,7 +61,7 @@ class Server_Add_Controller extends Admin_Controller {
}
if (!is_dir($file)) {
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
- if (!in_array($ext, legal_file::get_extensions())) {
+ if (!legal_file::get_extensions($ext)) {
continue;
}
}
@@ -169,7 +169,7 @@ class Server_Add_Controller extends Admin_Controller {
foreach ($child_paths as $child_path) {
if (!is_dir($child_path)) {
$ext = strtolower(pathinfo($child_path, PATHINFO_EXTENSION));
- if (!in_array($ext, legal_file::get_extensions()) || !filesize($child_path)) {
+ if (!legal_file::get_extensions($ext) || !filesize($child_path)) {
// Not importable, skip it.
continue;
}
@@ -255,7 +255,7 @@ class Server_Add_Controller extends Admin_Controller {
} else {
try {
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
- if (in_array($extension, legal_file::get_photo_extensions())) {
+ if (legal_file::get_photo_extensions($extension)) {
$photo = ORM::factory("item");
$photo->type = "photo";
$photo->parent_id = $parent->id;
@@ -265,7 +265,7 @@ class Server_Add_Controller extends Admin_Controller {
$photo->owner_id = $owner_id;
$photo->save();
$entry->item_id = $photo->id;
- } else if (in_array($extension, legal_file::get_movie_extensions())) {
+ } else if (legal_file::get_movie_extensions($extension)) {
$movie = ORM::factory("item");
$movie->type = "movie";
$movie->parent_id = $parent->id;
diff --git a/modules/server_add/helpers/server_add_installer.php b/modules/server_add/helpers/server_add_installer.php
index e843fc79..b62bbcfa 100644
--- a/modules/server_add/helpers/server_add_installer.php
+++ b/modules/server_add/helpers/server_add_installer.php
@@ -30,7 +30,6 @@ class server_add_installer {
`task_id` int(9) NOT NULL,
PRIMARY KEY (`id`))
DEFAULT CHARSET=utf8;");
- module::set_version("server_add", 4);
server_add::check_config();
}
diff --git a/modules/slideshow/helpers/slideshow_installer.php b/modules/slideshow/helpers/slideshow_installer.php
index d283487d..22bd9534 100644
--- a/modules/slideshow/helpers/slideshow_installer.php
+++ b/modules/slideshow/helpers/slideshow_installer.php
@@ -20,7 +20,6 @@
class slideshow_installer {
static function install() {
module::set_var("slideshow", "max_scale", 0);
- module::set_version("slideshow", 2);
}
static function upgrade($version) {
diff --git a/modules/tag/controllers/tag.php b/modules/tag/controllers/tag.php
index 6199c49b..bada9bac 100644
--- a/modules/tag/controllers/tag.php
+++ b/modules/tag/controllers/tag.php
@@ -86,6 +86,7 @@ class Tag_Controller extends Controller {
"previous_item" => $previous_item,
"next_item" => $next_item,
"sibling_count" => $tag->items_count($where),
+ "siblings_callback" => array(array($tag, "items"), array()),
"breadcrumbs" => array(
Breadcrumb::instance($root->title, $root->url())->set_first(),
Breadcrumb::instance(t("Tag: %tag_name", array("tag_name" => $tag->name)),
diff --git a/modules/tag/controllers/tags.php b/modules/tag/controllers/tags.php
index 77ad7f50..77d45a95 100644
--- a/modules/tag/controllers/tags.php
+++ b/modules/tag/controllers/tags.php
@@ -52,7 +52,7 @@ class Tags_Controller extends Controller {
$limit = Input::instance()->get("limit");
$tag_part = ltrim(end($tag_parts));
$tag_list = ORM::factory("tag")
- ->where("name", "LIKE", "{$tag_part}%")
+ ->where("name", "LIKE", Database::escape_for_like($tag_part) . "%")
->order_by("name", "ASC")
->limit($limit)
->find_all();
diff --git a/modules/tag/helpers/tag_installer.php b/modules/tag/helpers/tag_installer.php
index f80a9de3..1fd18f3e 100644
--- a/modules/tag/helpers/tag_installer.php
+++ b/modules/tag/helpers/tag_installer.php
@@ -37,7 +37,6 @@ class tag_installer {
KEY(`item_id`, `id`))
DEFAULT CHARSET=utf8;");
module::set_var("tag", "tag_cloud_size", 30);
- module::set_version("tag", 3);
}
static function upgrade($version) {
diff --git a/modules/user/helpers/user_installer.php b/modules/user/helpers/user_installer.php
index e28af69a..67f6a3d5 100644
--- a/modules/user/helpers/user_installer.php
+++ b/modules/user/helpers/user_installer.php
@@ -138,6 +138,5 @@ class user_installer {
access::allow($registered, "view_full", $root);
module::set_var("user", "minimum_password_length", 5);
- module::set_version("user", 4);
}
} \ No newline at end of file
diff --git a/modules/watermark/controllers/admin_watermarks.php b/modules/watermark/controllers/admin_watermarks.php
index 0e6e214b..b058d6a5 100644
--- a/modules/watermark/controllers/admin_watermarks.php
+++ b/modules/watermark/controllers/admin_watermarks.php
@@ -66,7 +66,7 @@ class Admin_Watermarks_Controller extends Admin_Controller {
$form = watermark::get_delete_form();
if ($form->validate()) {
- if ($name = module::get_var("watermark", "name")) {
+ if ($name = basename(module::get_var("watermark", "name"))) {
@unlink(VARPATH . "modules/watermark/$name");
module::clear_var("watermark", "name");
@@ -93,39 +93,30 @@ class Admin_Watermarks_Controller extends Admin_Controller {
access::verify_csrf();
$form = watermark::get_add_form();
- if ($form->validate()) {
+ // For TEST_MODE, we want to simulate a file upload. Because this is not a true upload, Forge's
+ // validation logic will correctly reject it. So, we skip validation when we're running tests.
+ if (TEST_MODE || $form->validate()) {
$file = $_POST["file"];
- $pathinfo = pathinfo($file);
// Forge prefixes files with "uploadfile-xxxxxxx" for uniqueness
- $name = preg_replace("/uploadfile-[^-]+-(.*)/", '$1', $pathinfo["basename"]);
- $name = legal_file::smash_extensions($name);
-
- if (!($image_info = getimagesize($file)) ||
- !in_array($image_info[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG))) {
- message::error(t("Unable to identify this image file"));
+ $name = preg_replace("/uploadfile-[^-]+-(.*)/", '$1', basename($file));
+
+ try {
+ list ($width, $height, $mime_type, $extension) = photo::get_file_metadata($file);
+ // Sanitize filename, which ensures a valid extension. This renaming prevents the issues
+ // addressed in ticket #1855, where an image that looked valid (header said jpg) with a
+ // php extension was previously accepted without changing its extension.
+ $name = legal_file::sanitize_filename($name, $extension, "photo");
+ } catch (Exception $e) {
+ message::error(t("Invalid or unidentifiable image file"));
@unlink($file);
return;
}
- if (!in_array($pathinfo["extension"], legal_file::get_photo_extensions())) {
- switch ($image_info[2]) {
- case IMAGETYPE_GIF:
- $name = legal_file::change_extension($name, "gif");
- break;
- case IMAGETYPE_JPEG:
- $name = legal_file::change_extension($name, "jpg");
- break;
- case IMAGETYPE_PNG:
- $name = legal_file::change_extension($name, "png");
- break;
- }
- }
-
rename($file, VARPATH . "modules/watermark/$name");
module::set_var("watermark", "name", $name);
- module::set_var("watermark", "width", $image_info[0]);
- module::set_var("watermark", "height", $image_info[1]);
- module::set_var("watermark", "mime_type", $image_info["mime"]);
+ module::set_var("watermark", "width", $width);
+ module::set_var("watermark", "height", $height);
+ module::set_var("watermark", "mime_type", $mime_type);
module::set_var("watermark", "position", $form->add_watermark->position->value);
module::set_var("watermark", "transparency", $form->add_watermark->transparency->value);
$this->_update_graphics_rules();
diff --git a/modules/watermark/helpers/watermark_installer.php b/modules/watermark/helpers/watermark_installer.php
index 5df780a1..13338912 100644
--- a/modules/watermark/helpers/watermark_installer.php
+++ b/modules/watermark/helpers/watermark_installer.php
@@ -33,7 +33,6 @@ class watermark_installer {
DEFAULT CHARSET=utf8;");
@mkdir(VARPATH . "modules/watermark");
- module::set_version("watermark", 2);
}
static function uninstall() {
diff --git a/modules/watermark/tests/Admin_Watermarks_Controller_Test.php b/modules/watermark/tests/Admin_Watermarks_Controller_Test.php
new file mode 100644
index 00000000..0b4ba84b
--- /dev/null
+++ b/modules/watermark/tests/Admin_Watermarks_Controller_Test.php
@@ -0,0 +1,124 @@
+<?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 Admin_Watermarks_Controller_Test extends Gallery_Unit_Test_Case {
+ public function setup() {
+ $this->_save = array($_POST, $_SERVER);
+ $_SERVER["HTTP_REFERER"] = "HTTP_REFERER";
+ }
+
+ public function teardown() {
+ list($_POST, $_SERVER) = $this->_save;
+ }
+
+ public function add_watermark_test() {
+ // Source is a jpg file, watermark path has extension jpg
+ $name = test::random_name();
+ $source_path = MODPATH . "gallery/images/imagemagick.jpg";
+ $watermark_path = TMPPATH . "uploadfile-123-{$name}.jpg";
+ copy($source_path, $watermark_path);
+
+ // Setup and run Admin_Watermarks_Controller::add
+ $controller = new Admin_Watermarks_Controller();
+ $_POST["file"] = $watermark_path;
+ $_POST["csrf"] = access::csrf_token();
+ ob_start();
+ $controller->add();
+ $results = ob_get_clean();
+
+ // Add should be successful
+ $this->assert_equal(json_encode(array("result" => "success",
+ "location" => url::site("admin/watermarks"))), $results);
+ $this->assert_equal(file_get_contents($source_path),
+ file_get_contents(VARPATH . "modules/watermark/$name.jpg"));
+ $this->assert_equal("$name.jpg", module::get_var("watermark", "name"));
+ $this->assert_equal(114, module::get_var("watermark", "width"));
+ $this->assert_equal(118, module::get_var("watermark", "height"));
+ $this->assert_equal("image/jpeg", module::get_var("watermark", "mime_type"));
+ }
+
+ public function add_watermark_reject_illegal_file_test() {
+ // Source is a php file, watermark path has extension php
+ $name = test::random_name();
+ $source_path = MODPATH . "watermark/tests/Admin_Watermarks_Controller_Test.php";
+ $watermark_path = TMPPATH . "uploadfile-123-{$name}.php";
+ copy($source_path, $watermark_path);
+
+ // Setup and run Admin_Watermarks_Controller::add
+ $controller = new Admin_Watermarks_Controller();
+ $_POST["file"] = $watermark_path;
+ $_POST["csrf"] = access::csrf_token();
+ ob_start();
+ $controller->add();
+ $results = ob_get_clean();
+
+ // Add should *not* be successful, and watermark should be deleted
+ $this->assert_equal("", $results);
+ $this->assert_false(file_exists($watermark_path));
+ $this->assert_false(file_exists(VARPATH . "modules/watermark/$name.php"));
+ }
+
+ public function add_watermark_rename_legal_file_with_illegal_extension_test() {
+ // Source is a jpg file, watermark path has extension php
+ $name = test::random_name();
+ $source_path = MODPATH . "gallery/images/imagemagick.jpg";
+ $watermark_path = TMPPATH . "uploadfile-123-{$name}.php";
+ copy($source_path, $watermark_path);
+
+ // Setup and run Admin_Watermarks_Controller::add
+ $controller = new Admin_Watermarks_Controller();
+ $_POST["file"] = $watermark_path;
+ $_POST["csrf"] = access::csrf_token();
+ ob_start();
+ $controller->add();
+ $results = ob_get_clean();
+
+ // Add should be successful with file renamed as jpg
+ $this->assert_equal(json_encode(array("result" => "success",
+ "location" => url::site("admin/watermarks"))), $results);
+ $this->assert_equal(file_get_contents($source_path),
+ file_get_contents(VARPATH . "modules/watermark/$name.jpg"));
+ $this->assert_equal("$name.jpg", module::get_var("watermark", "name"));
+ $this->assert_equal(114, module::get_var("watermark", "width"));
+ $this->assert_equal(118, module::get_var("watermark", "height"));
+ $this->assert_equal("image/jpeg", module::get_var("watermark", "mime_type"));
+ }
+
+ public function add_watermark_reject_illegal_file_with_legal_extension_test() {
+ // Source is a php file, watermark path has extension jpg
+ $name = test::random_name();
+ $source_path = MODPATH . "watermark/tests/Admin_Watermarks_Controller_Test.php";
+ $watermark_path = TMPPATH . "uploadfile-123-{$name}.jpg";
+ copy($source_path, $watermark_path);
+
+ // Setup and run Admin_Watermarks_Controller::add
+ $controller = new Admin_Watermarks_Controller();
+ $_POST["file"] = $watermark_path;
+ $_POST["csrf"] = access::csrf_token();
+ ob_start();
+ $controller->add();
+ $results = ob_get_clean();
+
+ // Add should *not* be successful, and watermark should be deleted
+ $this->assert_equal("", $results);
+ $this->assert_false(file_exists($watermark_path));
+ $this->assert_false(file_exists(VARPATH . "modules/watermark/$name.php"));
+ $this->assert_false(file_exists(VARPATH . "modules/watermark/$name.jpg"));
+ }
+}
diff --git a/system/helpers/url.php b/system/helpers/url.php
index 1bc81230..014f96fe 100644
--- a/system/helpers/url.php
+++ b/system/helpers/url.php
@@ -62,7 +62,7 @@ class url_Core {
if ($site_domain === '' OR $site_domain[0] === '/')
{
// Guess the server name if the domain starts with slash
- $base_url = $protocol.'://'.$_SERVER['HTTP_HOST'].$site_domain;
+ $base_url = $protocol.'://'.($_SERVER['SERVER_NAME']?$_SERVER['SERVER_NAME']:$_SERVER['HTTP_HOST']).$site_domain;
}
else
{
diff --git a/system/libraries/ORM.php b/system/libraries/ORM.php
index eb34221c..16e047bc 100644
--- a/system/libraries/ORM.php
+++ b/system/libraries/ORM.php
@@ -1469,6 +1469,7 @@ class ORM_Core {
// Load the result
$result = $this->db_builder->execute($this->db);
+ $this->db_applied = array();
if ($array === TRUE)
{
diff --git a/themes/admin_wind/css/fix-ie.css b/themes/admin_wind/css/fix-ie.css
index 5475cb79..4546f9fa 100644
--- a/themes/admin_wind/css/fix-ie.css
+++ b/themes/admin_wind/css/fix-ie.css
@@ -15,4 +15,4 @@ tr.g-info td,
tr.g-success td,
tr.g-warning td {
background: none !important;
-}
+}