From 28b41056e3ea962dce1ad017a3c0a60252195e7a Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Wed, 27 May 2009 15:07:27 -0700 Subject: Restructure things so that the application is now just another module. Kohana makes this type of transition fairly straightforward in that all controllers/helpers/etc are still located in the cascading filesystem without any extra effort, except that I've temporarily added a hack to force modules/gallery into the module path. Rename what's left of "core" to be "application" so that it conforms more closely to the Kohana standard (basically, just application/config/config.php which is the minimal thing that you need in the application directory) There's still considerable work left to be done here. --- modules/gallery/config/cookie.php | 49 ++ modules/gallery/config/database.php | 23 + modules/gallery/config/locale.php | 46 ++ modules/gallery/config/routes.php | 31 + modules/gallery/config/sendmail.php | 29 + modules/gallery/config/session.php | 66 +++ modules/gallery/config/upload.php | 36 ++ modules/gallery/controllers/admin.php | 52 ++ .../controllers/admin_advanced_settings.php | 53 ++ modules/gallery/controllers/admin_dashboard.php | 93 +++ modules/gallery/controllers/admin_graphics.php | 63 +++ modules/gallery/controllers/admin_languages.php | 136 +++++ modules/gallery/controllers/admin_maintenance.php | 181 ++++++ modules/gallery/controllers/admin_modules.php | 65 +++ .../gallery/controllers/admin_theme_details.php | 67 +++ modules/gallery/controllers/admin_themes.php | 79 +++ modules/gallery/controllers/after_install.php | 30 + modules/gallery/controllers/albums.php | 229 ++++++++ modules/gallery/controllers/file_proxy.php | 120 ++++ modules/gallery/controllers/items.php | 30 + modules/gallery/controllers/l10n_client.php | 128 +++++ modules/gallery/controllers/maintenance.php | 24 + modules/gallery/controllers/move.php | 64 +++ modules/gallery/controllers/movies.php | 114 ++++ modules/gallery/controllers/permissions.php | 80 +++ modules/gallery/controllers/photos.php | 116 ++++ modules/gallery/controllers/quick.php | 122 ++++ modules/gallery/controllers/rest.php | 183 ++++++ modules/gallery/controllers/scaffold.php | 437 ++++++++++++++ modules/gallery/controllers/simple_uploader.php | 86 +++ modules/gallery/css/debug.css | 28 + modules/gallery/css/l10n_client.css | 185 ++++++ modules/gallery/css/quick.css | 40 ++ modules/gallery/helpers/MY_remote.php | 163 ++++++ modules/gallery/helpers/MY_url.php | 81 +++ modules/gallery/helpers/access.php | 628 +++++++++++++++++++++ modules/gallery/helpers/album.php | 132 +++++ modules/gallery/helpers/batch.php | 40 ++ modules/gallery/helpers/block_manager.php | 67 +++ modules/gallery/helpers/core.php | 52 ++ modules/gallery/helpers/core_block.php | 100 ++++ modules/gallery/helpers/core_event.php | 46 ++ modules/gallery/helpers/core_installer.php | 278 +++++++++ modules/gallery/helpers/core_menu.php | 162 ++++++ modules/gallery/helpers/core_search.php | 24 + modules/gallery/helpers/core_task.php | 166 ++++++ modules/gallery/helpers/core_theme.php | 137 +++++ modules/gallery/helpers/dir.php | 40 ++ modules/gallery/helpers/graphics.php | 387 +++++++++++++ modules/gallery/helpers/item.php | 105 ++++ modules/gallery/helpers/l10n_client.php | 203 +++++++ modules/gallery/helpers/l10n_scanner.php | 154 +++++ modules/gallery/helpers/locale.php | 119 ++++ modules/gallery/helpers/log.php | 108 ++++ modules/gallery/helpers/message.php | 108 ++++ modules/gallery/helpers/model_cache.php | 46 ++ modules/gallery/helpers/module.php | 357 ++++++++++++ modules/gallery/helpers/movie.php | 153 +++++ modules/gallery/helpers/photo.php | 171 ++++++ modules/gallery/helpers/rest.php | 116 ++++ modules/gallery/helpers/site_status.php | 132 +++++ modules/gallery/helpers/task.php | 83 +++ modules/gallery/helpers/theme.php | 61 ++ modules/gallery/helpers/xml.php | 35 ++ modules/gallery/hooks/init_gallery.php | 44 ++ modules/gallery/images/gallery.png | Bin 0 -> 22178 bytes modules/gallery/images/gd.png | Bin 0 -> 5531 bytes modules/gallery/images/graphicsmagick.png | Bin 0 -> 1486 bytes modules/gallery/images/imagemagick.jpg | Bin 0 -> 20337 bytes modules/gallery/js/albums_form_add.js | 12 + modules/gallery/js/fullsize.js | 78 +++ modules/gallery/js/l10n_client.js | 195 +++++++ modules/gallery/js/quick.js | 95 ++++ modules/gallery/libraries/Admin_View.php | 126 +++++ modules/gallery/libraries/Block.php | 30 + modules/gallery/libraries/I18n.php | 410 ++++++++++++++ modules/gallery/libraries/MY_Database.php | 92 +++ modules/gallery/libraries/MY_Forge.php | 59 ++ modules/gallery/libraries/MY_ORM.php | 46 ++ modules/gallery/libraries/MY_Pagination.php | 35 ++ modules/gallery/libraries/MY_View.php | 46 ++ modules/gallery/libraries/Menu.php | 187 ++++++ modules/gallery/libraries/ORM_MPTT.php | 307 ++++++++++ modules/gallery/libraries/Sendmail.php | 97 ++++ modules/gallery/libraries/Task_Definition.php | 50 ++ modules/gallery/libraries/Theme_View.php | 221 ++++++++ modules/gallery/models/access_cache.php | 21 + modules/gallery/models/access_intent.php | 21 + modules/gallery/models/graphics_rule.php | 21 + modules/gallery/models/incoming_translation.php | 21 + modules/gallery/models/item.php | 497 ++++++++++++++++ modules/gallery/models/log.php | 22 + modules/gallery/models/message.php | 21 + modules/gallery/models/module.php | 21 + modules/gallery/models/outgoing_translation.php | 21 + modules/gallery/models/permission.php | 21 + modules/gallery/models/task.php | 46 ++ modules/gallery/models/theme.php | 21 + modules/gallery/models/var.php | 21 + modules/gallery/module.info | 3 + modules/gallery/tests/Access_Helper_Test.php | 323 +++++++++++ modules/gallery/tests/Album_Helper_Test.php | 87 +++ modules/gallery/tests/Albums_Controller_Test.php | 76 +++ modules/gallery/tests/Core_Installer_Test.php | 50 ++ modules/gallery/tests/Database_Test.php | 134 +++++ modules/gallery/tests/Dir_Helper_Test.php | 32 ++ modules/gallery/tests/DrawForm_Test.php | 84 +++ modules/gallery/tests/File_Structure_Test.php | 235 ++++++++ modules/gallery/tests/I18n_Test.php | 108 ++++ modules/gallery/tests/Item_Model_Test.php | 143 +++++ modules/gallery/tests/Menu_Test.php | 32 ++ modules/gallery/tests/Movie_Helper_Test.php | 46 ++ modules/gallery/tests/ORM_MPTT_Test.php | 221 ++++++++ modules/gallery/tests/Photo_Helper_Test.php | 109 ++++ modules/gallery/tests/Photos_Controller_Test.php | 74 +++ modules/gallery/tests/REST_Controller_Test.php | 197 +++++++ modules/gallery/tests/REST_Helper_Test.php | 45 ++ modules/gallery/tests/Sendmail_Test.php | 115 ++++ modules/gallery/tests/Var_Test.php | 49 ++ modules/gallery/tests/images/DSC_0003.jpg | Bin 0 -> 735609 bytes modules/gallery/tests/images/DSC_0005.jpg | Bin 0 -> 687555 bytes modules/gallery/tests/images/DSC_0017.jpg | Bin 0 -> 1246655 bytes modules/gallery/tests/images/DSC_0019.jpg | Bin 0 -> 649556 bytes modules/gallery/tests/images/DSC_0067.jpg | Bin 0 -> 1245526 bytes modules/gallery/tests/images/DSC_0072.jpg | Bin 0 -> 1014511 bytes modules/gallery/tests/images/P4050088.jpg | Bin 0 -> 1774906 bytes modules/gallery/tests/selenium/Add_Album.html | 52 ++ modules/gallery/tests/selenium/Add_Comment.html | 52 ++ modules/gallery/tests/selenium/Add_Item.html | 62 ++ modules/gallery/tests/selenium/Login.html | 47 ++ modules/gallery/tests/test.jpg | Bin 0 -> 6232 bytes .../gallery/views/admin_advanced_settings.html.php | 34 ++ .../gallery/views/admin_block_log_entries.html.php | 11 + modules/gallery/views/admin_block_news.html.php | 11 + .../views/admin_block_photo_stream.html.php | 14 + .../gallery/views/admin_block_platform.html.php | 18 + modules/gallery/views/admin_block_stats.html.php | 12 + modules/gallery/views/admin_block_welcome.html.php | 20 + modules/gallery/views/admin_dashboard.html.php | 38 ++ modules/gallery/views/admin_graphics.html.php | 28 + modules/gallery/views/admin_graphics_gd.html.php | 29 + .../views/admin_graphics_graphicsmagick.html.php | 21 + .../views/admin_graphics_imagemagick.html.php | 21 + modules/gallery/views/admin_graphics_none.html.php | 7 + modules/gallery/views/admin_languages.html.php | 15 + modules/gallery/views/admin_maintenance.html.php | 181 ++++++ .../gallery/views/admin_maintenance_task.html.php | 32 ++ modules/gallery/views/admin_modules.html.php | 32 ++ modules/gallery/views/admin_theme_details.html.php | 6 + modules/gallery/views/admin_themes.html.php | 89 +++ .../gallery/views/admin_themes_preview.html.php | 7 + modules/gallery/views/after_install.html.php | 29 + .../gallery/views/after_install_loader.html.php | 7 + modules/gallery/views/form.html.php | 75 +++ modules/gallery/views/kohana_error_page.php | 118 ++++ modules/gallery/views/kohana_profiler.php | 35 ++ modules/gallery/views/l10n_client.html.php | 31 + modules/gallery/views/maintenance.html.php | 50 ++ modules/gallery/views/move_browse.html.php | 47 ++ modules/gallery/views/move_tree.html.php | 19 + modules/gallery/views/permissions_browse.html.php | 56 ++ modules/gallery/views/permissions_form.html.php | 94 +++ modules/gallery/views/quick_pane.html.php | 108 ++++ modules/gallery/views/scaffold.html.php | 169 ++++++ modules/gallery/views/simple_uploader.html.php | 249 ++++++++ 165 files changed, 14533 insertions(+) create mode 100644 modules/gallery/config/cookie.php create mode 100644 modules/gallery/config/database.php create mode 100644 modules/gallery/config/locale.php create mode 100644 modules/gallery/config/routes.php create mode 100644 modules/gallery/config/sendmail.php create mode 100644 modules/gallery/config/session.php create mode 100644 modules/gallery/config/upload.php create mode 100644 modules/gallery/controllers/admin.php create mode 100644 modules/gallery/controllers/admin_advanced_settings.php create mode 100644 modules/gallery/controllers/admin_dashboard.php create mode 100644 modules/gallery/controllers/admin_graphics.php create mode 100644 modules/gallery/controllers/admin_languages.php create mode 100644 modules/gallery/controllers/admin_maintenance.php create mode 100644 modules/gallery/controllers/admin_modules.php create mode 100644 modules/gallery/controllers/admin_theme_details.php create mode 100644 modules/gallery/controllers/admin_themes.php create mode 100644 modules/gallery/controllers/after_install.php create mode 100644 modules/gallery/controllers/albums.php create mode 100644 modules/gallery/controllers/file_proxy.php create mode 100644 modules/gallery/controllers/items.php create mode 100644 modules/gallery/controllers/l10n_client.php create mode 100644 modules/gallery/controllers/maintenance.php create mode 100644 modules/gallery/controllers/move.php create mode 100644 modules/gallery/controllers/movies.php create mode 100644 modules/gallery/controllers/permissions.php create mode 100644 modules/gallery/controllers/photos.php create mode 100644 modules/gallery/controllers/quick.php create mode 100644 modules/gallery/controllers/rest.php create mode 100644 modules/gallery/controllers/scaffold.php create mode 100644 modules/gallery/controllers/simple_uploader.php create mode 100644 modules/gallery/css/debug.css create mode 100644 modules/gallery/css/l10n_client.css create mode 100644 modules/gallery/css/quick.css create mode 100644 modules/gallery/helpers/MY_remote.php create mode 100644 modules/gallery/helpers/MY_url.php create mode 100644 modules/gallery/helpers/access.php create mode 100644 modules/gallery/helpers/album.php create mode 100644 modules/gallery/helpers/batch.php create mode 100644 modules/gallery/helpers/block_manager.php create mode 100644 modules/gallery/helpers/core.php create mode 100644 modules/gallery/helpers/core_block.php create mode 100644 modules/gallery/helpers/core_event.php create mode 100644 modules/gallery/helpers/core_installer.php create mode 100644 modules/gallery/helpers/core_menu.php create mode 100644 modules/gallery/helpers/core_search.php create mode 100644 modules/gallery/helpers/core_task.php create mode 100644 modules/gallery/helpers/core_theme.php create mode 100644 modules/gallery/helpers/dir.php create mode 100644 modules/gallery/helpers/graphics.php create mode 100644 modules/gallery/helpers/item.php create mode 100644 modules/gallery/helpers/l10n_client.php create mode 100644 modules/gallery/helpers/l10n_scanner.php create mode 100644 modules/gallery/helpers/locale.php create mode 100644 modules/gallery/helpers/log.php create mode 100644 modules/gallery/helpers/message.php create mode 100644 modules/gallery/helpers/model_cache.php create mode 100644 modules/gallery/helpers/module.php create mode 100644 modules/gallery/helpers/movie.php create mode 100644 modules/gallery/helpers/photo.php create mode 100644 modules/gallery/helpers/rest.php create mode 100644 modules/gallery/helpers/site_status.php create mode 100644 modules/gallery/helpers/task.php create mode 100644 modules/gallery/helpers/theme.php create mode 100644 modules/gallery/helpers/xml.php create mode 100644 modules/gallery/hooks/init_gallery.php create mode 100644 modules/gallery/images/gallery.png create mode 100644 modules/gallery/images/gd.png create mode 100644 modules/gallery/images/graphicsmagick.png create mode 100644 modules/gallery/images/imagemagick.jpg create mode 100644 modules/gallery/js/albums_form_add.js create mode 100644 modules/gallery/js/fullsize.js create mode 100644 modules/gallery/js/l10n_client.js create mode 100644 modules/gallery/js/quick.js create mode 100644 modules/gallery/libraries/Admin_View.php create mode 100644 modules/gallery/libraries/Block.php create mode 100644 modules/gallery/libraries/I18n.php create mode 100644 modules/gallery/libraries/MY_Database.php create mode 100644 modules/gallery/libraries/MY_Forge.php create mode 100644 modules/gallery/libraries/MY_ORM.php create mode 100644 modules/gallery/libraries/MY_Pagination.php create mode 100644 modules/gallery/libraries/MY_View.php create mode 100644 modules/gallery/libraries/Menu.php create mode 100644 modules/gallery/libraries/ORM_MPTT.php create mode 100644 modules/gallery/libraries/Sendmail.php create mode 100644 modules/gallery/libraries/Task_Definition.php create mode 100644 modules/gallery/libraries/Theme_View.php create mode 100644 modules/gallery/models/access_cache.php create mode 100644 modules/gallery/models/access_intent.php create mode 100644 modules/gallery/models/graphics_rule.php create mode 100644 modules/gallery/models/incoming_translation.php create mode 100644 modules/gallery/models/item.php create mode 100644 modules/gallery/models/log.php create mode 100644 modules/gallery/models/message.php create mode 100644 modules/gallery/models/module.php create mode 100644 modules/gallery/models/outgoing_translation.php create mode 100644 modules/gallery/models/permission.php create mode 100644 modules/gallery/models/task.php create mode 100644 modules/gallery/models/theme.php create mode 100644 modules/gallery/models/var.php create mode 100644 modules/gallery/module.info create mode 100644 modules/gallery/tests/Access_Helper_Test.php create mode 100644 modules/gallery/tests/Album_Helper_Test.php create mode 100644 modules/gallery/tests/Albums_Controller_Test.php create mode 100644 modules/gallery/tests/Core_Installer_Test.php create mode 100644 modules/gallery/tests/Database_Test.php create mode 100644 modules/gallery/tests/Dir_Helper_Test.php create mode 100644 modules/gallery/tests/DrawForm_Test.php create mode 100644 modules/gallery/tests/File_Structure_Test.php create mode 100644 modules/gallery/tests/I18n_Test.php create mode 100644 modules/gallery/tests/Item_Model_Test.php create mode 100644 modules/gallery/tests/Menu_Test.php create mode 100644 modules/gallery/tests/Movie_Helper_Test.php create mode 100644 modules/gallery/tests/ORM_MPTT_Test.php create mode 100644 modules/gallery/tests/Photo_Helper_Test.php create mode 100644 modules/gallery/tests/Photos_Controller_Test.php create mode 100644 modules/gallery/tests/REST_Controller_Test.php create mode 100644 modules/gallery/tests/REST_Helper_Test.php create mode 100644 modules/gallery/tests/Sendmail_Test.php create mode 100644 modules/gallery/tests/Var_Test.php create mode 100644 modules/gallery/tests/images/DSC_0003.jpg create mode 100644 modules/gallery/tests/images/DSC_0005.jpg create mode 100644 modules/gallery/tests/images/DSC_0017.jpg create mode 100644 modules/gallery/tests/images/DSC_0019.jpg create mode 100644 modules/gallery/tests/images/DSC_0067.jpg create mode 100644 modules/gallery/tests/images/DSC_0072.jpg create mode 100644 modules/gallery/tests/images/P4050088.jpg create mode 100644 modules/gallery/tests/selenium/Add_Album.html create mode 100644 modules/gallery/tests/selenium/Add_Comment.html create mode 100644 modules/gallery/tests/selenium/Add_Item.html create mode 100644 modules/gallery/tests/selenium/Login.html create mode 100644 modules/gallery/tests/test.jpg create mode 100644 modules/gallery/views/admin_advanced_settings.html.php create mode 100644 modules/gallery/views/admin_block_log_entries.html.php create mode 100644 modules/gallery/views/admin_block_news.html.php create mode 100644 modules/gallery/views/admin_block_photo_stream.html.php create mode 100644 modules/gallery/views/admin_block_platform.html.php create mode 100644 modules/gallery/views/admin_block_stats.html.php create mode 100644 modules/gallery/views/admin_block_welcome.html.php create mode 100644 modules/gallery/views/admin_dashboard.html.php create mode 100644 modules/gallery/views/admin_graphics.html.php create mode 100644 modules/gallery/views/admin_graphics_gd.html.php create mode 100644 modules/gallery/views/admin_graphics_graphicsmagick.html.php create mode 100644 modules/gallery/views/admin_graphics_imagemagick.html.php create mode 100644 modules/gallery/views/admin_graphics_none.html.php create mode 100644 modules/gallery/views/admin_languages.html.php create mode 100644 modules/gallery/views/admin_maintenance.html.php create mode 100644 modules/gallery/views/admin_maintenance_task.html.php create mode 100644 modules/gallery/views/admin_modules.html.php create mode 100644 modules/gallery/views/admin_theme_details.html.php create mode 100644 modules/gallery/views/admin_themes.html.php create mode 100644 modules/gallery/views/admin_themes_preview.html.php create mode 100644 modules/gallery/views/after_install.html.php create mode 100644 modules/gallery/views/after_install_loader.html.php create mode 100644 modules/gallery/views/form.html.php create mode 100644 modules/gallery/views/kohana_error_page.php create mode 100644 modules/gallery/views/kohana_profiler.php create mode 100644 modules/gallery/views/l10n_client.html.php create mode 100644 modules/gallery/views/maintenance.html.php create mode 100644 modules/gallery/views/move_browse.html.php create mode 100644 modules/gallery/views/move_tree.html.php create mode 100644 modules/gallery/views/permissions_browse.html.php create mode 100644 modules/gallery/views/permissions_form.html.php create mode 100644 modules/gallery/views/quick_pane.html.php create mode 100644 modules/gallery/views/scaffold.html.php create mode 100644 modules/gallery/views/simple_uploader.html.php (limited to 'modules') diff --git a/modules/gallery/config/cookie.php b/modules/gallery/config/cookie.php new file mode 100644 index 00000000..692ef548 --- /dev/null +++ b/modules/gallery/config/cookie.php @@ -0,0 +1,49 @@ + email address that appears as the from address + * line-length => word wrap length (PHP documentations suggest no larger tha 70 characters + * reply-to => what goes into the reply to header + */ +$config["from"] = "admin@gallery3.com"; +$config["line_length"] = 70; +$config["reply_to"] = "public@gallery3.com"; +$config["header_separator"] = "\n"; diff --git a/modules/gallery/config/session.php b/modules/gallery/config/session.php new file mode 100644 index 00000000..990fa31f --- /dev/null +++ b/modules/gallery/config/session.php @@ -0,0 +1,66 @@ +admin)) { + throw new Exception("@todo UNAUTHORIZED", 401); + } + parent::__construct(); + } + + public function __call($controller_name, $args) { + if (request::method() == "post") { + access::verify_csrf(); + } + + if ($controller_name == "index") { + $controller_name = "dashboard"; + } + $controller_name = "Admin_{$controller_name}_Controller"; + if ($args) { + $method = array_shift($args); + } else { + $method = "index"; + } + + if (!method_exists($controller_name, $method)) { + return kohana::show_404(); + } + + call_user_func_array(array(new $controller_name, $method), $args); + } +} + diff --git a/modules/gallery/controllers/admin_advanced_settings.php b/modules/gallery/controllers/admin_advanced_settings.php new file mode 100644 index 00000000..79bc1183 --- /dev/null +++ b/modules/gallery/controllers/admin_advanced_settings.php @@ -0,0 +1,53 @@ +content = new View("admin_advanced_settings.html"); + $view->content->vars = ORM::factory("var") + ->orderby("module_name", "name") + ->find_all(); + print $view; + } + + public function edit($module_name, $var_name) { + $value = module::get_var($module_name, $var_name); + $form = new Forge("admin/advanced_settings/save/$module_name/$var_name", "", "post"); + $group = $form->group("edit_var")->label( + t("Edit %var (%module_name)", + array("module_name" => $module_name, "var" => $var_name))); + $group->input("module_name")->label(t("Module"))->value($module_name)->disabled(1); + $group->input("var_name")->label(t("Setting"))->value($var_name)->disabled(1); + $group->textarea("value")->label(t("Value"))->value($value); + $group->submit("")->value(t("Save")); + print $form; + } + + public function save($module_name, $var_name) { + access::verify_csrf(); + + module::set_var($module_name, $var_name, Input::instance()->post("value")); + message::success( + t("Saved value for %var (%module_name)", + array("var" => $var_name, "module_name" => $module_name))); + + print json_encode(array("result" => "success")); + } +} diff --git a/modules/gallery/controllers/admin_dashboard.php b/modules/gallery/controllers/admin_dashboard.php new file mode 100644 index 00000000..d2d2f79b --- /dev/null +++ b/modules/gallery/controllers/admin_dashboard.php @@ -0,0 +1,93 @@ +content = new View("admin_dashboard.html"); + $view->content->blocks = block_manager::get_html("dashboard_center"); + $view->sidebar = "
" . + block_manager::get_html("dashboard_sidebar") . + "
"; + print $view; + } + + public function add_block() { + $form = core_block::get_add_block_form(); + if ($form->validate()) { + list ($module_name, $id) = explode(":", $form->add_block->id->value); + $available = block_manager::get_available(); + + if ($form->add_block->center->value) { + block_manager::add("dashboard_center", $module_name, $id); + message::success( + t("Added %title block to the dashboard center", + array("title" => $available["$module_name:$id"]))); + } else { + block_manager::add("dashboard_sidebar", $module_name, $id); + message::success( + t("Added %title to the dashboard sidebar", + array("title" => $available["$module_name:$id"]))); + } + } + url::redirect("admin/dashboard"); + } + + public function remove_block($id) { + access::verify_csrf(); + $blocks_center = block_manager::get_active("dashboard_center"); + $blocks_sidebar = block_manager::get_active("dashboard_sidebar"); + + if (array_key_exists($id, $blocks_sidebar)) { + $deleted = $blocks_sidebar[$id]; + block_manager::remove("dashboard_sidebar", $id); + } else if (array_key_exists($id, $blocks_center)) { + $deleted = $blocks_center[$id]; + block_manager::remove("dashboard_center", $id); + } + + if (!empty($deleted)) { + $available = block_manager::get_available(); + $title = $available[join(":", $deleted)]; + message::success(t("Removed %title block", array("title" => $title))); + } + + url::redirect("admin"); + } + + public function reorder() { + access::verify_csrf(); + $active_set = array(); + foreach (array("dashboard_sidebar", "dashboard_center") as $location) { + foreach (block_manager::get_active($location) as $id => $info) { + $active_set[$id] = $info; + } + } + + foreach (array("dashboard_sidebar", "dashboard_center") as $location) { + $new_blocks = array(); + foreach ($this->input->get($location, array()) as $id) { + $new_blocks[$id] = $active_set[$id]; + } + block_manager::set_active($location, $new_blocks); + } + + $this->_force_block_adder(); + } +} diff --git a/modules/gallery/controllers/admin_graphics.php b/modules/gallery/controllers/admin_graphics.php new file mode 100644 index 00000000..0b3014f0 --- /dev/null +++ b/modules/gallery/controllers/admin_graphics.php @@ -0,0 +1,63 @@ +content = new View("admin_graphics.html"); + $view->content->available = ""; + + $tk = new ArrayObject(graphics::detect_toolkits(), ArrayObject::ARRAY_AS_PROPS); + $active = module::get_var("core", "graphics_toolkit", "none"); + foreach (array("gd", "imagemagick", "graphicsmagick", "none") as $id) { + if ($id == $active) { + $view->content->active = new View("admin_graphics_$id.html"); + $view->content->active->tk = $tk; + $view->content->active->is_active = true; + } else if ($id != "none") { + $v = new View("admin_graphics_$id.html"); + $v->tk = $tk; + $v->is_active = false; + $view->content->available .= $v; + } + } + + print $view; + } + + public function choose($toolkit) { + access::verify_csrf(); + if ($toolkit != module::get_var("core", "graphics_toolkit")) { + module::set_var("core", "graphics_toolkit", $toolkit); + + $toolkit_info = graphics::detect_toolkits(); + if ($toolkit == "graphicsmagick" || $toolkit == "imagemagick") { + module::set_var("core", "graphics_toolkit_path", $toolkit_info[$toolkit]); + } + + site_status::clear("missing_graphics_toolkit"); + message::success(t("Updated Graphics Toolkit")); + log::success("graphics", t("Changed graphics toolkit to: %toolkit", + array("toolkit" => $toolkit))); + } + + url::redirect("admin/graphics"); + } +} + diff --git a/modules/gallery/controllers/admin_languages.php b/modules/gallery/controllers/admin_languages.php new file mode 100644 index 00000000..37d335a3 --- /dev/null +++ b/modules/gallery/controllers/admin_languages.php @@ -0,0 +1,136 @@ +content = new View("admin_languages.html"); + $v->content->settings_form = $this->_languages_form(); + if (empty($share_translations_form)) { + $share_translations_form = $this->_share_translations_form(); + } + $v->content->share_translations_form = $share_translations_form; + $this->_outgoing_translations_count(); + print $v; + } + + public function save() { + $form = $this->_languages_form(); + if ($form->validate()) { + module::set_var("core", "default_locale", $form->choose_language->locale->value); + locale::update_installed($form->choose_language->installed_locales->value); + message::success(t("Settings saved")); + } + url::redirect("admin/languages"); + } + + public function share() { + $form = $this->_share_translations_form(); + if (!$form->validate()) { + // Show the page with form errors + return $this->index($form); + } + + if ($form->sharing->share) { + l10n_client::submit_translations(); + message::success(t("Translations submitted")); + } else { + return $this->_save_api_key($form); + } + url::redirect("admin/languages"); + } + + private function _save_api_key($form) { + $new_key = $form->sharing->api_key->value; + if ($new_key && !l10n_client::validate_api_key($new_key)) { + $form->sharing->api_key->add_error("invalid", 1); + $valid = false; + } else { + $valid = true; + } + + if ($valid) { + $old_key = l10n_client::api_key(); + l10n_client::api_key($new_key); + if ($old_key && !$new_key) { + message::success(t("Your API key has been cleared.")); + } else if ($old_key && $new_key && $old_key != $new_key) { + message::success(t("Your API key has been changed.")); + } else if (!$old_key && $new_key) { + message::success(t("Your API key has been saved.")); + } + + log::success(t("core"), t("l10n_client API key changed.")); + url::redirect("admin/languages"); + } else { + // Show the page with form errors + $this->index($form); + } + } + + private function _languages_form() { + $all_locales = locale::available(); + $installed_locales = locale::installed(); + $form = new Forge("admin/languages/save", "", "post", array("id" => "gLanguageSettingsForm")); + $group = $form->group("choose_language") + ->label(t("Language settings")); + $group->dropdown("locale") + ->options($installed_locales) + ->selected(module::get_var("core", "default_locale")) + ->label(t("Default language")) + ->rules('required'); + + $installation_options = array(); + foreach ($all_locales as $code => $display_name) { + $installation_options[$code] = array($display_name, isset($installed_locales->$code)); + } + $group->checklist("installed_locales") + ->label(t("Installed Languages")) + ->options($installation_options) + ->rules("required"); + $group->submit("save")->value(t("Save settings")); + return $form; + } + + private function _outgoing_translations_count() { + return ORM::factory("outgoing_translation")->count_all(); + } + + private function _share_translations_form() { + $form = new Forge("admin/languages/share", "", "post", array("id" => "gShareTranslationsForm")); + $group = $form->group("sharing") + ->label(t("Sharing you own translations with the Gallery community is easy. Please do!")); + $api_key = l10n_client::api_key(); + $server_link = l10n_client::server_api_key_url(); + $group->input("api_key") + ->label(empty($api_key) + ? t("This is a unique key that will allow you to send translations to the remote server. To get your API key go to %server-link.", + array("server-link" => html::anchor($server_link))) + : t("API Key")) + ->value($api_key) + ->error_messages("invalid", t("The API key you provided is invalid.")); + $group->submit("save")->value(t("Save settings")); + if ($api_key && $this->_outgoing_translations_count()) { + // TODO: UI improvement: hide API key / save button when API key is set. + $group->submit("share")->value(t("Submit translations")); + } + return $form; + } +} + diff --git a/modules/gallery/controllers/admin_maintenance.php b/modules/gallery/controllers/admin_maintenance.php new file mode 100644 index 00000000..c169de75 --- /dev/null +++ b/modules/gallery/controllers/admin_maintenance.php @@ -0,0 +1,181 @@ +query( + "UPDATE {tasks} SET `state` = 'stalled' " . + "WHERE done = 0 " . + "AND state <> 'stalled' " . + "AND unix_timestamp(now()) - updated > 15"); + $stalled_count = $query->count(); + if ($stalled_count) { + log::warning("tasks", + t2("One task is stalled", + "%count tasks are stalled", + $stalled_count), + t('view', + array("url" => url::site("admin/maintenance")))); + } + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_maintenance.html"); + $view->content->task_definitions = task::get_definitions(); + $view->content->running_tasks = ORM::factory("task") + ->where("done", 0)->orderby("updated", "DESC")->find_all(); + $view->content->finished_tasks = ORM::factory("task") + ->where("done", 1)->orderby("updated", "DESC")->find_all(); + print $view; + } + + /** + * Start a new task + * @param string $task_callback + */ + public function start($task_callback) { + access::verify_csrf(); + + $tasks = task::get_definitions(); + $task = task::create($tasks[$task_callback], array()); + $view = new View("admin_maintenance_task.html"); + $view->task = $task; + + log::info("tasks", t("Task %task_name started (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor(url::site("admin/maintenance"), t("maintenance"))); + print $view; + } + + /** + * Resume a stalled task + * @param string $task_id + */ + public function resume($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded) { + throw new Exception("@todo MISSING_TASK"); + } + $view = new View("admin_maintenance_task.html"); + $view->task = $task; + + log::info("tasks", t("Task %task_name resumed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor(url::site("admin/maintenance"), t("maintenance"))); + print $view; + } + + /** + * Cancel a task. + * @param string $task_id + */ + public function cancel($task_id) { + access::verify_csrf(); + + task::cancel($task_id); + + message::success(t("Task cancelled")); + url::redirect("admin/maintenance"); + } + + public function cancel_running_tasks() { + access::verify_csrf(); + Database::instance()->update( + "tasks", + array("done" => 1, "state" => "cancelled"), + array("done" => 0)); + message::success(t("All running tasks cancelled")); + url::redirect("admin/maintenance"); + } + + /** + * Remove a task. + * @param string $task_id + */ + public function remove($task_id) { + access::verify_csrf(); + + task::remove($task_id); + + message::success(t("Task removed")); + url::redirect("admin/maintenance"); + } + + public function remove_finished_tasks() { + access::verify_csrf(); + Database::instance()->delete("tasks", array("done" => 1)); + message::success(t("All finished tasks removed")); + url::redirect("admin/maintenance"); + } + + /** + * Run a task. This will trigger the task to do a small amount of work, then it will report + * back with status on the task. + * @param string $task_id + */ + public function run($task_id) { + access::verify_csrf(); + + try { + $task = task::run($task_id); + } catch (Exception $e) { + Kohana::log( + "error", + sprintf( + "%s in %s at line %s:\n%s", $e->getMessage(), $e->getFile(), + $e->getLine(), $e->getTraceAsString())); + throw $e; + } + + if ($task->done) { + switch ($task->state) { + case "success": + log::success("tasks", t("Task %task_name completed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor(url::site("admin/maintenance"), t("maintenance"))); + message::success(t("Task completed successfully")); + break; + + case "error": + log::error("tasks", t("Task %task_name failed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor(url::site("admin/maintenance"), t("maintenance"))); + message::success(t("Task failed")); + break; + } + print json_encode(array("result" => "success", + "task" => array( + "percent_complete" => $task->percent_complete, + "status" => $task->status, + "done" => $task->done), + "location" => url::site("admin/maintenance"))); + + } else { + print json_encode(array("result" => "in_progress", + "task" => array( + "percent_complete" => $task->percent_complete, + "status" => $task->status, + "done" => $task->done))); + } + } +} diff --git a/modules/gallery/controllers/admin_modules.php b/modules/gallery/controllers/admin_modules.php new file mode 100644 index 00000000..f7dd909d --- /dev/null +++ b/modules/gallery/controllers/admin_modules.php @@ -0,0 +1,65 @@ +content = new View("admin_modules.html"); + $view->content->available = module::available(); + print $view; + } + + public function save() { + access::verify_csrf(); + + $changes->activate = array(); + $changes->deactivate = array(); + $activated_names = array(); + $deactivated_names = array(); + foreach (module::available() as $module_name => $info) { + if ($info->locked) { + continue; + } + + $desired = $this->input->post($module_name) == 1; + if ($info->active && !$desired && module::is_active($module_name)) { + $changes->deactivate[] = $module_name; + $deactivated_names[] = $info->name; + module::deactivate($module_name); + } else if (!$info->active && $desired && !module::is_active($module_name)) { + $changes->activate[] = $module_name; + $activated_names[] = $info->name; + module::install($module_name); + module::activate($module_name); + } + } + + module::event("module_change", $changes); + + // @todo this type of collation is questionable from a i18n perspective + if ($activated_names) { + message::success(t("Activated: %names", array("names" => join(", ", $activated_names)))); + } + if ($deactivated_names) { + message::success(t("Deactivated: %names", array("names" => join(", ", $deactivated_names)))); + } + url::redirect("admin/modules"); + } +} + diff --git a/modules/gallery/controllers/admin_theme_details.php b/modules/gallery/controllers/admin_theme_details.php new file mode 100644 index 00000000..542ec31c --- /dev/null +++ b/modules/gallery/controllers/admin_theme_details.php @@ -0,0 +1,67 @@ +content = new View("admin_theme_details.html"); + $view->content->form = theme::get_edit_form_admin(); + print $view; + } + + public function save() { + $form = theme::get_edit_form_admin(); + if ($form->validate()) { + module::set_var("core", "page_size", $form->edit_theme->page_size->value); + + $thumb_size = $form->edit_theme->thumb_size->value; + $thumb_dirty = false; + if (module::get_var("core", "thumb_size") != $thumb_size) { + graphics::remove_rule("core", "thumb", "resize"); + graphics::add_rule( + "core", "thumb", "resize", + array("width" => $thumb_size, "height" => $thumb_size, "master" => Image::AUTO), + 100); + module::set_var("core", "thumb_size", $thumb_size); + } + + $resize_size = $form->edit_theme->resize_size->value; + $resize_dirty = false; + if (module::get_var("core", "resize_size") != $resize_size) { + graphics::remove_rule("core", "resize", "resize"); + graphics::add_rule( + "core", "resize", "resize", + array("width" => $resize_size, "height" => $resize_size, "master" => Image::AUTO), + 100); + module::set_var("core", "resize_size", $resize_size); + } + + module::set_var("core", "header_text", $form->edit_theme->header_text->value); + module::set_var("core", "footer_text", $form->edit_theme->footer_text->value); + + message::success(t("Updated theme details")); + url::redirect("admin/theme_details"); + } else { + $view = new Admin_View("admin.html"); + $view->content = $form; + print $view; + } + } +} + diff --git a/modules/gallery/controllers/admin_themes.php b/modules/gallery/controllers/admin_themes.php new file mode 100644 index 00000000..05c134d1 --- /dev/null +++ b/modules/gallery/controllers/admin_themes.php @@ -0,0 +1,79 @@ +content = new View("admin_themes.html"); + $view->content->admin = module::get_var("core", "active_admin_theme"); + $view->content->site = module::get_var("core", "active_site_theme"); + $view->content->themes = $this->_get_themes(); + print $view; + } + + private function _get_themes() { + $themes = array(); + foreach (scandir(THEMEPATH) as $theme_name) { + if ($theme_name[0] == ".") { + continue; + } + + $file = THEMEPATH . "$theme_name/theme.info"; + $theme_info = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS); + $themes[$theme_name] = $theme_info; + } + return $themes; + } + + public function preview($type, $theme_name) { + $view = new View("admin_themes_preview.html"); + $theme_name = preg_replace("/[^\w]/", "", $theme_name); + $view->info = new ArrayObject( + parse_ini_file(THEMEPATH . "$theme_name/theme.info"), ArrayObject::ARRAY_AS_PROPS); + $view->theme_name = $theme_name; + $view->type = $type; + if ($type == "admin") { + $view->url = url::site("admin?theme=$theme_name"); + } else { + $view->url = url::site("albums/1?theme=$theme_name"); + } + print $view; + } + + public function choose($type, $theme_name) { + access::verify_csrf(); + + $theme_name = preg_replace("/[^\w]/", "", $theme_name); + $info = new ArrayObject( + parse_ini_file(THEMEPATH . "$theme_name/theme.info"), ArrayObject::ARRAY_AS_PROPS); + + if ($type == "admin" && $info->admin) { + module::set_var("core", "active_admin_theme", $theme_name); + message::success(t("Successfully changed your admin theme to %theme_name", + array("theme_name" => $info->name))); + } else if ($type == "site" && $info->site) { + module::set_var("core", "active_site_theme", $theme_name); + message::success(t("Successfully changed your Gallery theme to %theme_name", + array("theme_name" => $info->name))); + } + + url::redirect("admin/themes"); + } +} + diff --git a/modules/gallery/controllers/after_install.php b/modules/gallery/controllers/after_install.php new file mode 100644 index 00000000..f066afe4 --- /dev/null +++ b/modules/gallery/controllers/after_install.php @@ -0,0 +1,30 @@ +admin) { + url::redirect("albums/1"); + } + + $v = new View("after_install.html"); + $v->user = user::active(); + print $v; + } +} diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php new file mode 100644 index 00000000..5b4d5979 --- /dev/null +++ b/modules/gallery/controllers/albums.php @@ -0,0 +1,229 @@ +id != 1) { + access::forbidden(); + } else { + print new Theme_View("login_page.html", "album"); + return; + } + } + + $page_size = module::get_var("core", "page_size", 9); + $show = $this->input->get("show"); + + if ($show) { + $index = $album->get_position($show); + $page = ceil($index / $page_size); + if ($page == 1) { + url::redirect("albums/$album->id"); + } else { + url::redirect("albums/$album->id?page=$page"); + } + } + + $page = $this->input->get("page", "1"); + $children_count = $album->viewable()->children_count(); + $offset = ($page - 1) * $page_size; + $max_pages = max(ceil($children_count / $page_size), 1); + + // Make sure that the page references a valid offset + if ($page < 1) { + url::redirect("albums/$album->id"); + } else if ($page > $max_pages) { + url::redirect("albums/$album->id?page=$max_pages"); + } + + $template = new Theme_View("page.html", "album"); + $template->set_global("page_size", $page_size); + $template->set_global("item", $album); + $template->set_global("children", $album->viewable()->children($page_size, $offset)); + $template->set_global("children_count", $children_count); + $template->set_global("parents", $album->parents()); + $template->content = new View("album.html"); + + // We can't use math in ORM or the query builder, so do this by hand. It's important + // that we do this with math, otherwise concurrent accesses will damage accuracy. + Database::instance()->query( + "UPDATE {items} SET `view_count` = `view_count` + 1 WHERE `id` = $album->id"); + + print $template; + } + + /** + * @see REST_Controller::_create($resource) + */ + public function _create($album) { + access::required("add", $album); + + switch ($this->input->post("type")) { + case "album": + return $this->_create_album($album); + + case "photo": + return $this->_create_photo($album); + + default: + access::forbidden(); + } + } + + private function _create_album($album) { + access::required("add", $album); + + $form = album::get_add_form($album); + if ($form->validate()) { + $new_album = album::create( + $album, + $this->input->post("name"), + $this->input->post("title", $this->input->post("name")), + $this->input->post("description"), + user::active()->id); + + log::success("content", "Created an album", + html::anchor("albums/$new_album->id", "view album")); + message::success(t("Created album %album_title", array("album_title" => $new_album->title))); + + print json_encode( + array("result" => "success", + "location" => url::site("albums/$new_album->id"), + "resource" => url::site("albums/$new_album->id"))); + } else { + print json_encode( + array("result" => "error", + "form" => $form->__toString() . html::script("core/js/albums_form_add.js"))); + } + } + + private function _create_photo($album) { + access::required("add", $album); + + // If we set the content type as JSON, it triggers saving the result as + // a document in the browser (well, in Chrome at least). + // @todo figure out why and fix this. + $form = photo::get_add_form($album); + if ($form->validate()) { + $photo = photo::create( + $album, + $this->input->post("file"), + $_FILES["file"]["name"], + $this->input->post("title", $this->input->post("name")), + $this->input->post("description"), + user::active()->id); + + log::success("content", "Added a photo", html::anchor("photos/$photo->id", "view photo")); + message::success(t("Added photo %photo_title", array("photo_title" => $photo->title))); + + print json_encode( + array("result" => "success", + "resource" => url::site("photos/$photo->id"), + "location" => url::site("photos/$photo->id"))); + } else { + print json_encode( + array("result" => "error", + "form" => $form->__toString())); + } + } + + /** + * @see REST_Controller::_update($resource) + */ + public function _update($album) { + access::required("edit", $album); + + $form = album::get_edit_form($album); + if ($valid = $form->validate()) { + // Make sure that there's not a conflict + if (Database::instance() + ->from("items") + ->where("parent_id", $album->parent_id) + ->where("id <>", $album->id) + ->where("name", $form->edit_album->dirname->value) + ->count_records()) { + $form->edit_album->dirname->add_error("conflict", 1); + $valid = false; + } + } + + // @todo + // @todo we need to make sure that filename / dirname components can't contain a / + // @todo + + if ($valid) { + $orig = clone $album; + $album->title = $form->edit_album->title->value; + $album->description = $form->edit_album->description->value; + $album->sort_column = $form->edit_album->sort_order->column->value; + $album->sort_order = $form->edit_album->sort_order->direction->value; + $album->rename($form->edit_album->dirname->value); + $album->save(); + + module::event("item_updated", $orig, $album); + + log::success("content", "Updated album", "id\">view"); + message::success(t("Saved album %album_title", array("album_title" => $album->title))); + + print json_encode( + array("result" => "success", + "location" => url::site("albums/$album->id"))); + } else { + print json_encode( + array("result" => "error", + "form" => $form->__toString())); + } + } + + /** + * @see REST_Controller::_form_add($parameters) + */ + public function _form_add($album_id) { + $album = ORM::factory("item", $album_id); + access::required("add", $album); + + switch ($this->input->get("type")) { + case "album": + print album::get_add_form($album) . + html::script("core/js/albums_form_add.js"); + break; + + case "photo": + print photo::get_add_form($album); + break; + + default: + kohana::show_404(); + } + } + + /** + * @see REST_Controller::_form_add($parameters) + */ + public function _form_edit($album) { + access::required("edit", $album); + + print album::get_edit_form($album); + } +} diff --git a/modules/gallery/controllers/file_proxy.php b/modules/gallery/controllers/file_proxy.php new file mode 100644 index 00000000..f3c5f109 --- /dev/null +++ b/modules/gallery/controllers/file_proxy.php @@ -0,0 +1,120 @@ +input->server("REQUEST_URI"); + $request_uri = preg_replace("/\?.*/", "", $request_uri); + + // var_uri: http://example.com/gallery3/var/ + $var_uri = url::file("var/"); + + // Make sure that the request is for a file inside var + $offset = strpos($request_uri, $var_uri); + if ($offset === false) { + kohana::show_404(); + } + + $file = substr($request_uri, strlen($var_uri)); + + // Make sure that we don't leave the var dir + if (strpos($file, "..") !== false) { + kohana::show_404(); + } + + // We only handle var/resizes and var/albums + $paths = explode("/", $file); + $type = $paths[0]; + if ($type != "resizes" && $type != "albums" && $type != "thumbs") { + kohana::show_404(); + } + + // If the last element is .album.jpg, pop that off since it's not a real item + if ($paths[count($paths)-1] == ".album.jpg") { + array_pop($paths); + } + if ($paths[count($paths)-1] == "") { + array_pop($paths); + } + + // Find all items that match the level and name, then iterate over those to find a match. + // In most cases we'll get it in one. Note that for the level calculation, we just count the + // size of $paths. $paths includes the type ("thumbs", etc) but it doesn't include the root, + // so it's a wash. + $count = count($paths); + $compare_file = VARPATH . $file; + $item = null; + foreach (ORM::factory("item") + ->where("name", $paths[$count - 1]) + ->where("level", $count) + ->find_all() as $match) { + if ($type == "albums") { + $match_file = $match->file_path(); + } else if ($type == "resizes") { + $match_file = $match->resize_path(); + } else { + $match_file = $match->thumb_path(); + } + if ($match_file == $compare_file) { + $item = $match; + break; + } + } + + if (!$item) { + kohana::show_404(); + } + + // Make sure we have access to the item + if (!access::can("view", $item)) { + kohana::show_404(); + } + + // Make sure we have view_full access to the original + if ($type == "albums" && !access::can("view_full", $item)) { + kohana::show_404(); + } + + // Don't try to load a directory + if ($type == "albums" && $item->is_album()) { + kohana::show_404(); + } + + if (!file_exists($match_file)) { + kohana::show_404(); + } + + // Dump out the image + header("Content-Type: $item->mime_type"); + Kohana::close_buffers(false); + $fd = fopen($match_file, "rb"); + fpassthru($fd); + fclose($fd); + } +} diff --git a/modules/gallery/controllers/items.php b/modules/gallery/controllers/items.php new file mode 100644 index 00000000..13891726 --- /dev/null +++ b/modules/gallery/controllers/items.php @@ -0,0 +1,30 @@ +url(array(), true)); + } +} diff --git a/modules/gallery/controllers/l10n_client.php b/modules/gallery/controllers/l10n_client.php new file mode 100644 index 00000000..17520051 --- /dev/null +++ b/modules/gallery/controllers/l10n_client.php @@ -0,0 +1,128 @@ +admin or access::forbidden(); + + $input = Input::instance(); + $message = $input->post("l10n-message-source"); + $translation = $input->post("l10n-edit-target"); + $key = I18n::get_message_key($message); + $locale = I18n::instance()->locale(); + + $entry = ORM::factory("outgoing_translation") + ->where(array("key" => $key, + "locale" => $locale)) + ->find(); + + if (!$entry->loaded) { + $entry->key = $key; + $entry->locale = $locale; + $entry->message = serialize($message); + $entry->base_revision = null; + } + + $entry->translation = serialize($translation); + + $entry_from_incoming = ORM::factory("incoming_translation") + ->where(array("key" => $key, + "locale" => $locale)) + ->find(); + + if (!$entry_from_incoming->loaded) { + $entry->base_revision = $entry_from_incoming->revision; + } + + $entry->save(); + + print json_encode(new stdClass()); + } + + public function toggle_l10n_mode() { + access::verify_csrf(); + + $session = Session::instance(); + $session->set("l10n_mode", + !$session->get("l10n_mode", false)); + + url::redirect("albums/1"); + } + + private static function _l10n_client_form() { + $form = new Forge("l10n_client/save", "", "post", array("id" => "gL10nClientSaveForm")); + $group = $form->group("l10n_message"); + $group->hidden("l10n-message-source")->value(""); + $group->textarea("l10n-edit-target"); + $group->submit("l10n-edit-save")->value(t("Save translation")); + // TODO(andy_st): Avoiding multiple submit buttons for now (hassle with jQuery form plugin). + // $group->submit("l10n-edit-copy")->value(t("Copy source")); + // $group->submit("l10n-edit-clear")->value(t("Clear")); + + return $form; + } + + private static function _l10n_client_search_form() { + $form = new Forge("l10n_client/search", "", "post", array("id" => "gL10nSearchForm")); + $group = $form->group("l10n_search"); + $group->input("l10n-search")->id("gL10nSearch"); + $group->submit("l10n-search-filter-clear")->value(t("X")); + + return $form; + } + + public static function l10n_form() { + $calls = I18n::instance()->call_log(); + + if ($calls) { + $string_list = array(); + foreach ($calls as $call) { + list ($message, $options) = $call; + // Note: Don't interpolate placeholders for the actual translation input field. + // TODO: Use $options to generate a preview. + if (is_array($message)) { + // TODO: Handle plural forms. + // Translate each message. If it has a plural form, get + // the current locale's plural rules and all plural translations. + continue; + } + $source = $message; + $translation = ''; + $options_for_raw_translation = array(); + if (isset($options['count'])) { + $options_for_raw_translation['count'] = $options['count']; + } + if (I18n::instance()->has_translation($message, $options_for_raw_translation)) { + $translation = I18n::instance()->translate($message, $options_for_raw_translation); + } + $string_list[] = array('source' => $source, + 'translation' => $translation); + } + + $v = new View('l10n_client.html'); + $v->string_list = $string_list; + $v->l10n_form = self::_l10n_client_form(); + $v->l10n_search_form = self::_l10n_client_search_form(); + return $v; + } + + return ''; + } +} diff --git a/modules/gallery/controllers/maintenance.php b/modules/gallery/controllers/maintenance.php new file mode 100644 index 00000000..b5f39bed --- /dev/null +++ b/modules/gallery/controllers/maintenance.php @@ -0,0 +1,24 @@ +source = $source; + $view->tree = $this->_get_tree_html($source, ORM::factory("item", 1)); + print $view; + } + + public function save($source_id) { + access::verify_csrf(); + $source = ORM::factory("item", $source_id); + $target = ORM::factory("item", $this->input->post("target_id")); + + item::move($source, $target); + + print json_encode( + array("result" => "success", + "location" => url::site("albums/{$target->id}"))); + } + + public function show_sub_tree($source_id, $target_id) { + $source = ORM::factory("item", $source_id); + $target = ORM::factory("item", $target_id); + access::required("edit", $source); + access::required("view", $target); + + print $this->_get_tree_html($source, $target); + } + + private function _get_tree_html($source, $target) { + $view = new View("move_tree.html"); + $view->source = $source; + $view->parent = $target; + $view->children = ORM::factory("item") + ->viewable() + ->where("type", "album") + ->where("parent_id", $target->id) + ->find_all(); + return $view; + } + +} diff --git a/modules/gallery/controllers/movies.php b/modules/gallery/controllers/movies.php new file mode 100644 index 00000000..55bbb0e5 --- /dev/null +++ b/modules/gallery/controllers/movies.php @@ -0,0 +1,114 @@ +viewable() + ->where("parent_id", $photo->parent_id) + ->where("id >", $photo->id) + ->orderby("id", "ASC") + ->find(); + $previous_item = ORM::factory("item") + ->viewable() + ->where("parent_id", $photo->parent_id) + ->where("id <", $photo->id) + ->orderby("id", "DESC") + ->find(); + $position = ORM::factory("item") + ->viewable() + ->where("parent_id", $photo->parent_id) + ->where("id <=", $photo->id) + ->count_all(); + + $template = new Theme_View("page.html", "photo"); + $template->set_global("item", $photo); + $template->set_global("children", array()); + $template->set_global("children_count", $photo->children_count()); + $template->set_global("parents", $photo->parents()); + $template->set_global("next_item", $next_item->loaded ? $next_item : null); + $template->set_global("previous_item", $previous_item->loaded ? $previous_item : null); + $template->set_global("sibling_count", $photo->parent()->children_count()); + $template->set_global("position", $position); + + $template->content = new View("movie.html"); + + $photo->view_count++; + $photo->save(); + + print $template; + } + + /** + * @see REST_Controller::_update($resource) + */ + public function _update($photo) { + access::required("edit", $photo); + + $form = photo::get_edit_form($photo); + if ($valid = $form->validate()) { + // Make sure that there's not a conflict + if (Database::instance() + ->from("items") + ->where("parent_id", $photo->parent_id) + ->where("id <>", $photo->id) + ->where("name", $form->edit_photo->filename->value) + ->count_records()) { + $form->edit_photo->filename->add_error("conflict", 1); + $valid = false; + } + } + + if ($valid) { + $orig = clone $photo; + $photo->title = $form->edit_photo->title->value; + $photo->description = $form->edit_photo->description->value; + $photo->rename($form->edit_photo->filename->value); + $photo->save(); + + module::event("item_updated", $orig, $photo); + + log::success("content", "Updated photo", "id\">view"); + message::success(t("Saved photo %photo_title", array("photo_title" => $photo->title))); + + print json_encode( + array("result" => "success", + "location" => url::site("photos/$photo->id"))); + } else { + print json_encode( + array("result" => "error", + "form" => $form->__toString())); + } + } + + /** + * @see REST_Controller::_form_edit($resource) + */ + public function _form_edit($photo) { + access::required("edit", $photo); + print photo::get_edit_form($photo); + } +} diff --git a/modules/gallery/controllers/permissions.php b/modules/gallery/controllers/permissions.php new file mode 100644 index 00000000..b0cee303 --- /dev/null +++ b/modules/gallery/controllers/permissions.php @@ -0,0 +1,80 @@ +is_album()) { + access::forbidden(); + } + + $view = new View("permissions_browse.html"); + $view->htaccess_works = access::htaccess_works(); + $view->item = $item; + $view->parents = $item->parents(); + $view->form = $this->_get_form($item); + + print $view; + } + + function form($id) { + $item = ORM::factory("item", $id); + access::required("edit", $item); + + if (!$item->is_album()) { + access::forbidden(); + } + + print $this->_get_form($item); + } + + function change($command, $group_id, $perm_id, $item_id) { + access::verify_csrf(); + $group = ORM::factory("group", $group_id); + $perm = ORM::factory("permission", $perm_id); + $item = ORM::factory("item", $item_id); + access::required("edit", $item); + + if ($group->loaded && $perm->loaded && $item->loaded) { + switch($command) { + case "allow": + access::allow($group, $perm->name, $item); + break; + + case "deny": + access::deny($group, $perm->name, $item); + break; + + case "reset": + access::reset($group, $perm->name, $item); + break; + } + } + } + + function _get_form($item) { + $view = new View("permissions_form.html"); + $view->item = $item; + $view->groups = ORM::factory("group")->find_all(); + $view->permissions = ORM::factory("permission")->find_all(); + return $view; + } +} diff --git a/modules/gallery/controllers/photos.php b/modules/gallery/controllers/photos.php new file mode 100644 index 00000000..5d4040cf --- /dev/null +++ b/modules/gallery/controllers/photos.php @@ -0,0 +1,116 @@ +viewable() + ->where("parent_id", $photo->parent_id) + ->where("id >", $photo->id) + ->orderby("id", "ASC") + ->find(); + $previous_item = ORM::factory("item") + ->viewable() + ->where("parent_id", $photo->parent_id) + ->where("id <", $photo->id) + ->orderby("id", "DESC") + ->find(); + $position = ORM::factory("item") + ->viewable() + ->where("parent_id", $photo->parent_id) + ->where("id <=", $photo->id) + ->count_all(); + + $template = new Theme_View("page.html", "photo"); + $template->set_global("item", $photo); + $template->set_global("children", array()); + $template->set_global("children_count", $photo->children_count()); + $template->set_global("parents", $photo->parents()); + $template->set_global("next_item", $next_item->loaded ? $next_item : null); + $template->set_global("previous_item", $previous_item->loaded ? $previous_item : null); + $template->set_global("sibling_count", $photo->parent()->children_count()); + $template->set_global("position", $position); + + $template->content = new View("photo.html"); + + $photo->view_count++; + $photo->save(); + + print $template; + } + + /** + * @see REST_Controller::_update($resource) + */ + public function _update($photo) { + access::required("edit", $photo); + + $form = photo::get_edit_form($photo); + if ($valid = $form->validate()) { + if ($form->edit_photo->filename->value != $photo->name) { + // Make sure that there's not a conflict + if (Database::instance() + ->from("items") + ->where("parent_id", $photo->parent_id) + ->where("id <>", $photo->id) + ->where("name", $form->edit_photo->filename->value) + ->count_records()) { + $form->edit_photo->filename->add_error("conflict", 1); + $valid = false; + } + } + } + + if ($valid) { + $orig = clone $photo; + $photo->title = $form->edit_photo->title->value; + $photo->description = $form->edit_photo->description->value; + $photo->rename($form->edit_photo->filename->value); + $photo->save(); + + module::event("item_updated", $orig, $photo); + + log::success("content", "Updated photo", "id\">view"); + message::success(t("Saved photo %photo_title", array("photo_title" => $photo->title))); + + print json_encode( + array("result" => "success", + "location" => url::site("photos/$photo->id"))); + } else { + print json_encode( + array("result" => "error", + "form" => $form->__toString())); + } + } + + /** + * @see REST_Controller::_form_edit($resource) + */ + public function _form_edit($photo) { + access::required("edit", $photo); + print photo::get_edit_form($photo); + } +} diff --git a/modules/gallery/controllers/quick.php b/modules/gallery/controllers/quick.php new file mode 100644 index 00000000..643dce30 --- /dev/null +++ b/modules/gallery/controllers/quick.php @@ -0,0 +1,122 @@ +loaded) { + return ""; + } + + $view = new View("quick_pane.html"); + $view->item = $item; + $view->page_type = Input::instance()->get("page_type"); + print $view; + } + + public function rotate($id, $dir) { + access::verify_csrf(); + $item = ORM::factory("item", $id); + if (!$item->loaded) { + return ""; + } + + $degrees = 0; + switch($dir) { + case "ccw": + $degrees = -90; + break; + + case "cw": + $degrees = 90; + break; + } + + if ($degrees) { + graphics::rotate($item->file_path(), $item->file_path(), array("degrees" => $degrees)); + + list($item->width, $item->height) = getimagesize($item->file_path()); + $item->resize_dirty= 1; + $item->thumb_dirty= 1; + $item->save(); + + graphics::generate($item); + + $parent = $item->parent(); + if ($parent->album_cover_item_id == $item->id) { + copy($item->thumb_path(), $parent->thumb_path()); + $parent->thumb_width = $item->thumb_width; + $parent->thumb_height = $item->thumb_height; + $parent->save(); + } + } + + if (Input::instance()->get("page_type") == "album") { + print json_encode( + array("src" => $item->thumb_url() . "?rnd=" . rand(), + "width" => $item->thumb_width, + "height" => $item->thumb_height)); + } else { + print json_encode( + array("src" => $item->resize_url() . "?rnd=" . rand(), + "width" => $item->resize_width, + "height" => $item->resize_height)); + } + } + + public function make_album_cover($id) { + access::verify_csrf(); + item::make_album_cover(ORM::factory("item", $id)); + + print json_encode(array("result" => "success")); + } + + public function delete($id) { + access::verify_csrf(); + $item = ORM::factory("item", $id); + access::required("edit", $item); + + if ($item->is_album()) { + $msg = t("Deleted album %title", array("title" => $item->title)); + } else { + $msg = t("Deleted photo %title", array("title" => $item->title)); + } + + $item->delete(); + message::success($msg); + + if (Input::instance()->get("page_type") == "album") { + print json_encode(array("result" => "success", "reload" => 1)); + } else { + print json_encode(array("result" => "success", + "location" => url::site("albums/$parent->id"))); + } + } + + public function form_edit($id) { + $item = ORM::factory("item", $id); + access::required("edit", $item); + if ($item->is_album()) { + $form = album::get_edit_form($item); + } else { + $form = photo::get_edit_form($item); + } + print $form; + } +} diff --git a/modules/gallery/controllers/rest.php b/modules/gallery/controllers/rest.php new file mode 100644 index 00000000..11a6bbac --- /dev/null +++ b/modules/gallery/controllers/rest.php @@ -0,0 +1,183 @@ +resource_type == null) { + throw new Exception("@todo ERROR_MISSING_RESOURCE_TYPE"); + } + parent::__construct(); + } + + /** + * Handle dispatching for all REST controllers. + */ + public function __call($function, $args) { + // If no parameter was provided after the controller name (eg "/albums") then $function will + // be set to "index". Otherwise, $function is the first parameter, and $args are all + // subsequent parameters. + $request_method = rest::request_method(); + if ($function == "index" && $request_method == "get") { + return $this->_index(); + } + + $resource = ORM::factory($this->resource_type, (int)$function); + if (!$resource->loaded && $request_method != "post") { + return Kohana::show_404(); + } + + if ($request_method != "get") { + access::verify_csrf(); + } + + switch ($request_method) { + case "get": + return $this->_show($resource); + + case "put": + return $this->_update($resource); + + case "delete": + return $this->_delete($resource); + + case "post": + return $this->_create($resource); + } + } + + /* We're editing an existing item, load it from the database. */ + public function form_edit($resource_id) { + if ($this->resource_type == null) { + throw new Exception("@todo ERROR_MISSING_RESOURCE_TYPE"); + } + + // @todo this needs security checks + $resource = ORM::factory($this->resource_type, $resource_id); + if (!$resource->loaded) { + return Kohana::show_404(); + } + + return $this->_form_edit($resource); + } + + /* We're adding a new item, pass along any additional parameters. */ + public function form_add($parameters) { + return $this->_form_add($parameters); + } + + /** + * Perform a GET request on the controller root + * (e.g. http://www.example.com/gallery3/comments) + */ + public function _index() { + throw new Exception("@todo _create NOT IMPLEMENTED"); + } + + /** + * Perform a POST request on this resource + * @param ORM $resource the instance of this resource type + */ + public function _create($resource) { + throw new Exception("@todo _create NOT IMPLEMENTED"); + } + + /** + * Perform a GET request on this resource + * @param ORM $resource the instance of this resource type + */ + public function _show($resource) { + throw new Exception("@todo _show NOT IMPLEMENTED"); + } + + /** + * Perform a PUT request on this resource + * @param ORM $resource the instance of this resource type + */ + public function _update($resource) { + throw new Exception("@todo _update NOT IMPLEMENTED"); + } + + /** + * Perform a DELETE request on this resource + * @param ORM $resource the instance of this resource type + */ + public function _delete($resource) { + throw new Exception("@todo _delete NOT IMPLEMENTED"); + } + + /** + * Present a form for adding a new resource + * @param string part of the URI after the controller name + */ + public function _form_add($parameter) { + throw new Exception("@todo _form_add NOT IMPLEMENTED"); + } + + /** + * Present a form for editing an existing resource + * @param ORM $resource the resource container for instances of this resource type + */ + public function _form_edit($resource) { + throw new Exception("@todo _form_edit NOT IMPLEMENTED"); + } +} diff --git a/modules/gallery/controllers/scaffold.php b/modules/gallery/controllers/scaffold.php new file mode 100644 index 00000000..f0063725 --- /dev/null +++ b/modules/gallery/controllers/scaffold.php @@ -0,0 +1,437 @@ +template->album_count = ORM::factory("item")->where("type", "album")->count_all(); + $this->template->photo_count = ORM::factory("item")->where("type", "photo")->count_all(); + $this->template->album_tree = $this->_load_album_tree(); + $this->template->add_photo_html = $this->_get_add_photo_html(); + } catch (Exception $e) { + $this->template->album_count = 0; + $this->template->photo_count = 0; + $this->template->deepest_photo = null; + $this->template->album_tree = array(); + $this->template->add_photo_html = ""; + } + + $this->_load_comment_info(); + $this->_load_tag_info(); + + restore_error_handler(); + + if (!empty($session) && $session->get("profiler", false)) { + $profiler = new Profiler(); + $profiler->render(); + } + } + + + function add_photos() { + $path = trim($this->input->post("path")); + $parent_id = (int)$this->input->post("parent_id"); + $parent = ORM::factory("item", $parent_id); + if (!$parent->loaded) { + throw new Exception("@todo BAD_ALBUM"); + } + + batch::start(); + cookie::set("add_photos_path", $path); + $photo_count = 0; + foreach (glob("$path/*.[Jj][Pp][Gg]") as $file) { + set_time_limit(30); + photo::create($parent, $file, basename($file), basename($file)); + $photo_count++; + } + batch::stop(); + + if ($photo_count > 0) { + log::success("content", "(scaffold) Added $photo_count photos", + html::anchor("albums/$parent_id", "View album")); + } + + url::redirect("scaffold"); + } + + function add_albums_and_photos($count, $desired_type=null) { + srand(time()); + $parents = ORM::factory("item")->where("type", "album")->find_all()->as_array(); + $owner_id = user::active()->id; + + $test_images = glob(APPPATH . "tests/images/*.[Jj][Pp][Gg]"); + + batch::start(); + $album_count = $photo_count = 0; + for ($i = 0; $i < $count; $i++) { + set_time_limit(30); + + $parent = $parents[array_rand($parents)]; + $parent->reload(); + $type = $desired_type; + if (!$type) { + $type = rand(0, 10) ? "photo" : "album"; + } + if ($type == "album") { + $thumb_size = module::get_var("core", "thumb_size"); + $parents[] = album::create( + $parent, "rnd_" . rand(), "Rnd $i", "random album $i", $owner_id) + ->save(); + $album_count++; + } else { + $photo_index = rand(0, count($test_images) - 1); + photo::create($parent, $test_images[$photo_index], basename($test_images[$photo_index]), + "rnd_" . rand(), "sample thumb", $owner_id); + $photo_count++; + } + } + batch::stop(); + + if ($photo_count > 0) { + log::success("content", "(scaffold) Added $photo_count photos"); + } + + if ($album_count > 0) { + log::success("content", "(scaffold) Added $album_count albums"); + } + url::redirect("scaffold"); + } + + function random_phrase($count) { + static $words; + if (empty($words)) { + $sample_text = "Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium + laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt, explicabo. Nemo enim ipsam voluptatem, quia voluptas + sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione + voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit, + amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt, ut + labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis + nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi + consequatur? Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, quam + nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla + pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis + praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi + sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt + mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et + expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio, cumque + nihil impedit, quo minus id, quod maxime placeat, facere possimus, omnis voluptas + assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis + debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et + molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut + reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores + repellat."; + $words = preg_split('/\s+/', $sample_text); + } + + $chosen = array(); + for ($i = 0; $i < $count; $i++) { + $chosen[] = $words[array_rand($words)]; + } + + return implode(' ', $chosen); + } + + function add_comments($count) { + srand(time()); + $photos = ORM::factory("item")->where("type", "photo")->find_all()->as_array(); + $users = ORM::factory("user")->find_all()->as_array(); + + if (empty($photos)) { + url::redirect("scaffold"); + } + + if (module::is_active("akismet")) { + akismet::$test_mode = 1; + } + for ($i = 0; $i < $count; $i++) { + $photo = $photos[array_rand($photos)]; + $author = $users[array_rand($users)]; + $guest_name = ucfirst($this->random_phrase(rand(1, 3))); + $guest_email = sprintf("%s@%s.com", $this->random_phrase(1), $this->random_phrase(1)); + $guest_url = sprintf("http://www.%s.com", $this->random_phrase(1)); + comment::create($photo, $author, $this->random_phrase(rand(8, 500)), + $guest_name, $guest_email, $guest_url); + } + + url::redirect("scaffold"); + } + + function add_tags($count) { + $items = ORM::factory("item")->find_all()->as_array(); + + if (!empty($items)) { + $tags = $this->_generateTags($count); + + while ($count-- > 0) { + $tag_name = $tags[array_rand($tags)]; + $item = $items[array_rand($items)]; + + tag::add($item, $tag_name); + } + } + + url::redirect("scaffold"); + } + + private function _generateTags($number){ + // Words from lorem2.com + $words = explode( + " ", + "Lorem ipsum dolor sit amet consectetuer adipiscing elit Donec odio Quisque volutpat " . + "mattis eros Nullam malesuada erat ut turpis Suspendisse urna nibh viverra non " . + "semper suscipit posuere a pede Donec nec justo eget felis facilisis " . + "fermentum Aliquam porttitor mauris sit amet orci Aenean dignissim pellentesque " . + "felis Morbi in sem quis dui placerat ornare Pellentesque odio nisi euismod in " . + "pharetra a ultricies in diam Sed arcu Cras consequat Praesent dapibus neque " . + "id cursus faucibus tortor neque egestas augue eu vulputate magna eros eu " . + "erat Aliquam erat volutpat Nam dui mi tincidunt quis accumsan porttitor " . + "facilisis luctus metus Phasellus ultrices nulla quis nibh Quisque a " . + "lectus Donec consectetuer ligula vulputate sem tristique cursus Nam nulla quam " . + "gravida non commodo a sodales sit amet nisi Pellentesque fermentum " . + "dolor Aliquam quam lectus facilisis auctor ultrices ut elementum vulputate " . + "nunc Sed adipiscing ornare risus Morbi est est blandit sit amet sagittis vel " . + "euismod vel velit Pellentesque egestas sem Suspendisse commodo ullamcorper " . + "magna"); + + while ($number--) { + $results[] = $words[array_rand($words, 1)]; + } + return $results; + } + + function _error_handler($x) { + } + + private function _load_comment_info() { + if (class_exists("Comment_Model")) { + $this->template->comment_count = ORM::factory("comment")->count_all(); + } else { + $this->template->comment_count = 0; + } + } + + private function _load_tag_info() { + if (class_exists("Tag_Model")) { + $this->template->tag_count = ORM::factory("tag")->count_all(); + $this->template->most_tagged = Database::instance() + ->select("item_id AS id", "COUNT(tag_id) AS count") + ->from("items_tags") + ->groupby("item_id") + ->orderby("count", "DESC") + ->limit(1) + ->get() + ->current(); + } else { + $this->template->tag_count = 0; + $this->template->most_tagged = 0; + } + } + + function install($module_name, $redirect=true) { + $to_install = array(); + if ($module_name == "*") { + foreach (module::available() as $module_name => $info) { + if (empty($info->installed)) { + $to_install[] = $module_name; + } + } + } else { + $to_install[] = $module_name; + } + + foreach ($to_install as $module_name) { + if ($module_name != "core") { + require_once(DOCROOT . "modules/${module_name}/helpers/${module_name}_installer.php"); + } + module::install($module_name); + } + + if ($redirect) { + url::redirect("scaffold"); + } + } + + + public function package() { + $this->auto_render = false; + $db = Database::instance(); + + // Drop all tables + foreach ($db->list_tables() as $table) { + $db->query("DROP TABLE IF EXISTS `$table`"); + } + + // Clean out data + dir::unlink(VARPATH . "uploads"); + dir::unlink(VARPATH . "albums"); + dir::unlink(VARPATH . "resizes"); + dir::unlink(VARPATH . "thumbs"); + dir::unlink(VARPATH . "modules"); + dir::unlink(VARPATH . "tmp"); + + $db->clear_cache(); + module::$modules = array(); + module::$active = array(); + + // Use a known random seed so that subsequent packaging runs will reuse the same random + // numbers, keeping our install.sql file more stable. + srand(0); + + try { + core_installer::install(true); + module::load_modules(); + + foreach (array("user", "comment", "organize", "info", "rss", + "search", "slideshow", "tag") as $module_name) { + module::install($module_name); + module::activate($module_name); + } + } catch (Exception $e) { + Kohana::log("error", $e->getTraceAsString()); + print $e->getTrace(); + throw $e; + } + + url::redirect("scaffold/dump_database"); + } + + public function dump_database() { + $this->auto_render = false; + + // We now have a clean install with just the packages that we want. Make sure that the + // database is clean too. + $db = Database::instance(); + $db->query("TRUNCATE {sessions}"); + $db->query("TRUNCATE {logs}"); + $db->query("DELETE FROM {vars} WHERE `module_name` = 'core' AND `name` = '_cache'"); + $db->update("users", array("password" => ""), array("id" => 1)); + $db->update("users", array("password" => ""), array("id" => 2)); + + $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 --add-drop-table -h{$conn['host']} " . + "-u{$conn['user']} $pass {$conn['database']} > $sql_file"; + exec($command, $output, $status); + if ($status) { + print "
";
+      print "$command\n";
+      print "Failed to dump database\n";
+      print implode("\n", $output);
+      return;
+    }
+
+    // Post-process the sql file
+    $buf = "";
+    $root_timestamp = ORM::factory("item", 1)->created;
+    foreach (file($sql_file) as $line) {
+      // Prefix tables
+      $line = preg_replace(
+        "/(CREATE TABLE|IF EXISTS|INSERT INTO) `{$dbconfig['table_prefix']}(\w+)`/", "\\1 {\\2}",
+        $line);
+
+      // Normalize dates
+      $line = preg_replace("/,$root_timestamp,/", ",UNIX_TIMESTAMP(),", $line);
+      $buf .= $line;
+    }
+    $fd = fopen($sql_file, "wb");
+    fwrite($fd, $buf);
+    fclose($fd);
+
+    url::redirect("scaffold/dump_var");
+  }
+
+  public function dump_var() {
+    $this->auto_render = false;
+
+    $objects = new RecursiveIteratorIterator(
+      new RecursiveDirectoryIterator(VARPATH),
+      RecursiveIteratorIterator::SELF_FIRST);
+
+    $var_file = DOCROOT . "installer/init_var.php";
+    if (!is_writable($var_file)) {
+      print "$var_file is not writeable";
+      return;
+    }
+
+    $paths = array();
+    foreach($objects as $name => $file){
+      if ($file->getBasename() == "database.php") {
+        continue;
+      } else if (basename($file->getPath()) == "logs") {
+        continue;
+      }
+
+      if ($file->isDir()) {
+        $paths[] = "VARPATH . \"" . substr($name, strlen(VARPATH)) . "\"";
+      } else {
+        // @todo: serialize non-directories
+        print "Unknown file: $name";
+        return;
+      }
+    }
+    // Sort the paths so that the var file is stable
+    sort($paths);
+
+    $fd = fopen($var_file, "w");
+    fwrite($fd, "\n");
+    fwrite($fd, "where("type", "album")->find_all() as $album) {
+      if ($album->parent_id) {
+        $tree[$album->parent_id]->children[] = $album->id;
+      }
+      $tree[$album->id]->album = $album;
+      $tree[$album->id]->children = array();
+    }
+
+    return $tree;
+  }
+
+  public function form($arg1, $arg2) {
+    if ($arg1 == "add" && $arg2 == "photos") {
+      print $this->_get_add_photo_html();
+    }
+    $this->auto_render = false;
+  }
+
+  public function _get_add_photo_html($parent_id=1) {
+    $parent = ORM::factory("item", $parent_id);
+    return photo::get_add_form($parent);
+  }
+}
diff --git a/modules/gallery/controllers/simple_uploader.php b/modules/gallery/controllers/simple_uploader.php
new file mode 100644
index 00000000..bdf9582f
--- /dev/null
+++ b/modules/gallery/controllers/simple_uploader.php
@@ -0,0 +1,86 @@
+item = $item;
+    print $v;
+  }
+
+  public function start() {
+    batch::start();
+  }
+
+  public function add_photo($id) {
+    $album = ORM::factory("item", $id);
+    access::required("add", $album);
+    access::verify_csrf();
+
+    $file_validation = new Validation($_FILES);
+    $file_validation->add_rules("Filedata", "upload::valid", "upload::type[gif,jpg,png,flv,mp4]");
+    if ($file_validation->validate()) {
+
+      // SimpleUploader.swf does not yet call /start directly, so simulate it here for now.
+      if (!batch::in_progress()) {
+        batch::start();
+      }
+
+      $temp_filename = upload::save("Filedata");
+      try {
+        $name = substr(basename($temp_filename), 10);  // Skip unique identifier Kohana adds
+        $title = $this->convert_filename_to_title($name);
+        $path_info = pathinfo($temp_filename);
+        if (array_key_exists("extension", $path_info) &&
+            in_array(strtolower($path_info["extension"]), array("flv", "mp4"))) {
+          $movie = movie::create($album, $temp_filename, $name, $title);
+          log::success("content", t("Added a movie"),
+                       html::anchor("movies/$movie->id", t("view movie")));
+        } else {
+          $photo = photo::create($album, $temp_filename, $name, $title);
+          log::success("content", t("Added a photo"),
+                       html::anchor("photos/$photo->id", t("view photo")));
+        }
+      } catch (Exception $e) {
+        unlink($temp_filename);
+        throw $e;
+      }
+      unlink($temp_filename);
+    }
+    print "File Received";
+  }
+
+  /**
+   * We should move this into a helper somewhere.. but where is appropriate?
+   */
+  private function convert_filename_to_title($filename) {
+    $title = strtr($filename, "_", " ");
+    $title = preg_replace("/\..*?$/", "", $title);
+    $title = preg_replace("/ +/", " ", $title);
+    return $title;
+  }
+
+  public function finish() {
+    batch::stop();
+    print json_encode(array("result" => "success"));
+  }
+}
diff --git a/modules/gallery/css/debug.css b/modules/gallery/css/debug.css
new file mode 100644
index 00000000..fe5665ad
--- /dev/null
+++ b/modules/gallery/css/debug.css
@@ -0,0 +1,28 @@
+.gAnnotatedThemeBlock {
+  border: 1px solid #C00;
+  clear: both;
+  margin: 1em;
+  padding: 1em;
+  position: relative;
+}
+
+.gAnnotatedThemeBlock_album_top {
+  float: right;
+}
+
+.gAnnotatedThemeBlock_header_bottom {
+  float: right;
+}
+
+.gAnnotatedThemeBlock div.title {
+  background: #C00;
+  border: 1px solid black;
+  color: white;
+  font-size: 110%;
+  padding: 4px;
+  position: absolute;
+  right: -1em;
+  top: -1em;
+  text-align: left;
+  -moz-border-radius: 5%;
+}
diff --git a/modules/gallery/css/l10n_client.css b/modules/gallery/css/l10n_client.css
new file mode 100644
index 00000000..8973715f
--- /dev/null
+++ b/modules/gallery/css/l10n_client.css
@@ -0,0 +1,185 @@
+// TODO(andy_st): Add original copyright notice from Drupal l10_client.
+// TODO(andy_st): Add G3 copyright notice.
+// TODO(andy_st): clean up formatting to match our other CSS files.
+
+/* $Id: l10n_client.css,v 1.6 2008/09/09 10:48:20 goba Exp $ */
+
+/* width percentages add to 99% rather than 100% to prevent float
+overflows from occurring in an unnamed browser that can't decide
+how it wants to round. */
+
+/* l10n_client container */
+#l10n-client {
+  text-align:left;
+  z-index:99;
+  line-height:1em;
+  color:#000; background:#fff;
+  position:fixed;
+  width:100%; height: 2em;
+  bottom:0px; left:0px;
+  overflow:hidden;}
+
+  * html #l10n-client {
+    position:static;}
+
+#l10n-client-string-select .string-list,
+#l10n-client-string-editor .source,
+#l10n-client-string-editor .editor {
+  height:20em;}
+
+#l10n-client .labels {
+  overflow:hidden;
+  position:relative;
+  height:2em;
+  color:#fff;
+  background:#37a;}
+
+  #l10n-client .labels .label {
+    display:none;}
+
+  /* Panel toggle button (span) */
+  #l10n-client .labels .toggle {
+    cursor:pointer;
+    display:block;
+    position:absolute; right:0em;
+    padding: 0em .75em; height:2em; line-height:2em;
+    text-transform:uppercase;
+    text-align:center; background:#000;}
+
+  /* Panel labels */
+  #l10n-client h2 {
+    border-left:1px solid #fff;
+    height:1em; line-height:1em;
+    padding: .5em; margin:0px;
+    font-size:1em;
+    text-transform:uppercase;}
+
+    #l10n-client .strings h2 {
+      border:0px;}
+
+  /* 25 + 37 + 37 = 99 */
+  #l10n-client .strings {
+    width:25%; float:left;}
+
+  #l10n-client .source {
+    width:37%; float:left;}
+
+  #l10n-client .translation {
+    width:37%; float:left;}
+
+/* Translatable string list */
+#l10n-client-string-select {
+  display:none;
+  float:left;
+  width:25%;}
+
+  #l10n-client .string-list {
+    height:17em;
+    overflow:auto;
+    list-style:none; list-style-image:none;
+    margin:0em; padding:0em;}
+
+  #l10n-client .string-list li {
+    font-size:.9em;
+    line-height:1.5em;
+    cursor:default;
+    background:transparent;
+    list-style:none; list-style-image:none;
+    border-bottom:1px solid #ddd;
+    padding:.25em .5em;
+    margin:0em;}
+
+  /* Green for translated */
+  #l10n-client .string-list li.translated {
+    border-bottom-color:#9c3;
+    background:#cf6; color:#360;}
+
+    #l10n-client .string-list li.translated:hover {
+      background: #df8;}
+
+    #l10n-client .string-list li.translated:active {
+      background: #9c3;}
+
+  #l10n-client .string-list li.hidden {
+    display:none;}
+
+  /* Gray + Blue hover for untranslated */
+  #l10n-client .string-list li.untranslated {}
+
+    #l10n-client .string-list li.untranslated:hover {
+      background: #ace;}
+
+    #l10n-client .string-list li.untranslated:active {
+      background: #8ac;}
+
+  /* Selected string is indicated by bold text */
+  #l10n-client .string-list li.active {
+    font-weight:bold;}
+
+  #l10n-client #gL10nSearchForm {
+    background:#eee;
+    text-align:center;
+    height:2em; line-height:2em;
+    margin:0em; padding:.5em .5em;
+  }
+
+  #l10n-client #gL10nSearchForm .form-item,
+  #l10n-client #gL10nSearchForm input.form-text,
+  #l10n-client #gL10nSearchForm #search-filter-go,
+  #l10n-client #gL10nSearchForm #search-filter-clear {
+    display:inline;
+    vertical-align:middle;
+  }
+
+    #l10n-client #gL10nSearchForm .form-item {
+      margin:0em;
+      padding:0em;
+    }
+
+    #l10n-client #gL10nSearchForm input.form-text {
+      width:80%;
+    }
+
+    #l10n-client #gL10nSearchForm #search-filter-clear {
+      width:10%;
+      margin:0em;
+    }
+
+
+#l10n-client-string-editor {
+  display:none;
+  float:left;
+  width:74%;}
+
+  #l10n-client-string-editor .source {
+    overflow:hidden;
+    width:50%; float:left;}
+
+    #l10n-client-string-editor .source .source-text {
+      line-height:1.5em;
+      background:#eee;
+      height:16em; margin:1em; padding:1em;
+      overflow:auto;}
+
+  #l10n-client-string-editor .translation {
+    overflow:hidden;
+    width:49%; float:right;}
+
+#gL10nClientSaveForm {
+  padding:0em;}
+
+  #gL10nClientSaveForm .form-textarea {
+    height:13em;
+    font-size:1em; line-height:1.25em;
+    width:95%;}
+
+  #gL10nClientSaveForm .form-submit {
+    margin-top: 0em;}
+
+
+#l10n-client form ul,
+#l10n-client form li,
+#l10n-client form input[type=submit],
+#l10n-client form input[type=text] {
+  display: inline ! important ;
+}
diff --git a/modules/gallery/css/quick.css b/modules/gallery/css/quick.css
new file mode 100644
index 00000000..02f9953e
--- /dev/null
+++ b/modules/gallery/css/quick.css
@@ -0,0 +1,40 @@
+.gItem:hover {
+  background-color: #cfdeff;
+}
+
+.gQuick {
+  border: none !important;
+  margin: 0 !important;
+  padding: 0 !important;
+}
+
+#gQuickPane {
+  background: #000;
+  border-bottom: 1px solid #ccc;
+  opacity: 0.9;
+}
+
+#gQuickPane a {
+  cursor: pointer;
+  float: left;
+  margin: 4px;
+}
+
+#gQuickPaneOptions {
+  background: #000;
+  float: left;
+  width: 100%;
+}
+
+#gQuickPaneOptions li a {
+  display: block;
+  float: none;
+  width: auto;
+  margin: 0;
+  padding: .5em .5em .5em .8em;
+  text-align: left;
+}
+
+#gQuickPaneOptions li a:hover {
+  background-color: #4d4d4d;
+}
diff --git a/modules/gallery/helpers/MY_remote.php b/modules/gallery/helpers/MY_remote.php
new file mode 100644
index 00000000..4abf5bf1
--- /dev/null
+++ b/modules/gallery/helpers/MY_remote.php
@@ -0,0 +1,163 @@
+ $value) {
+      if (!empty($post_data_raw)) {
+        $post_data_raw .= '&';
+      }
+      $post_data_raw .= urlencode($key) . '=' . urlencode($value);
+    }
+    
+    $extra_headers['Content-Type'] = 'application/x-www-form-urlencoded';
+    $extra_headers['Content-Length'] = strlen($post_data_raw);
+    
+    return $post_data_raw;
+  }
+
+  /**
+   * A single request, without following redirects
+   *
+   * @todo: Handle redirects? If so, only for GET (i.e. not for POST), and use G2's WebHelper_simple::_parseLocation logic.
+   */
+  static function do_request($url, $method='GET', $headers=array(), $body='') {
+    /* Convert illegal characters */
+    $url = str_replace(' ', '%20', $url);
+    
+    $url_components = self::_parse_url_for_fsockopen($url);
+    $handle = fsockopen(
+      $url_components['fsockhost'], $url_components['port'], $errno, $errstr, 5);
+    if (empty($handle)) {
+      // log "Error $errno: '$errstr' requesting $url";
+      return array(null, null, null);
+    }
+    
+    $header_lines = array('Host: ' . $url_components['host']);
+    foreach ($headers as $key => $value) {
+      $header_lines[] = $key . ': ' . $value;
+    }
+    
+    $success = fwrite($handle, sprintf("%s %s HTTP/1.0\r\n%s\r\n\r\n%s",
+                                       $method,
+                                       $url_components['uri'],
+                                       implode("\r\n", $header_lines),
+                                       $body));
+    if (!$success) {
+      // Zero bytes written or false was returned
+      // log "fwrite failed in requestWebPage($url)" . ($success === false ? ' - false' : ''
+      return array(null, null, null);
+    }
+    fflush($handle);
+    
+    /*
+     * Read the status line.  fgets stops after newlines.  The first line is the protocol
+     * version followed by a numeric status code and its associated textual phrase.
+     */
+    $response_status = trim(fgets($handle, 4096));
+    if (empty($response_status)) {
+      // 'Empty http response code, maybe timeout'
+      return array(null, null, null);
+    }
+    
+    /* Read the headers */
+    $response_headers = array();
+    while (!feof($handle)) {
+      $line = trim(fgets($handle, 4096));
+      if (empty($line)) {
+        break;
+      }
+      
+      /* Normalize the line endings */
+      $line = str_replace("\r", '', $line);
+      
+      list ($key, $value) = explode(':', $line, 2);
+      if (isset($response_headers[$key])) {
+        if (!is_array($response_headers[$key])) {
+          $response_headers[$key] = array($response_headers[$key]);
+        }
+        $response_headers[$key][] = trim($value);
+      } else {
+        $response_headers[$key] = trim($value);
+      }
+    }
+    
+    /* Read the body */
+    $response_body = '';
+    while (!feof($handle)) {
+      $response_body .= fread($handle, 4096);
+    }
+    fclose($handle);
+
+    return array($response_status, $response_headers, $response_body);
+  }
+
+  /**
+   * Prepare for fsockopen call.
+   * @param string $url
+   * @return array url components
+   * @access private
+   */
+  private static function _parse_url_for_fsockopen($url) {
+    $url_components = parse_url($url);
+    if (strtolower($url_components['scheme']) == 'https') {
+      $url_components['fsockhost'] = 'ssl://' . $url_components['host'];
+      $default_port = 443;
+    } else {
+      $url_components['fsockhost'] = $url_components['host'];
+      $default_port = 80;
+    }
+    if (empty($url_components['port'])) {
+      $url_components['port'] = $default_port;
+    }
+    if (empty($url_components['path'])) {
+      $url_components['path'] = '/';
+    }
+    $uri = $url_components['path']
+      . (empty($url_components['query']) ? '' : '?' . $url_components['query']);
+    /* Unescape ampersands, since if the url comes from form input it will be escaped */
+    $url_components['uri'] = str_replace('&', '&', $uri);
+    return $url_components;
+  }
+}
+
diff --git a/modules/gallery/helpers/MY_url.php b/modules/gallery/helpers/MY_url.php
new file mode 100644
index 00000000..81dcbe1e
--- /dev/null
+++ b/modules/gallery/helpers/MY_url.php
@@ -0,0 +1,81 @@
+relative_path();
+    }
+    return parent::site($uri . $query, $protocol);
+  }
+
+  static function parse_url() {
+    if (Router::$controller) {
+      return;
+    }
+
+    $count = count(Router::$segments);
+    foreach (ORM::factory("item")
+             ->where("name", html_entity_decode(Router::$segments[$count - 1], ENT_QUOTES))
+             ->where("level", $count + 1)
+             ->find_all() as $match) {
+      if ($match->relative_path() == html_entity_decode(Router::$current_uri, ENT_QUOTES)) {
+        $item = $match;
+      }
+    }
+
+    if (!empty($item)) {
+      Router::$controller = "{$item->type}s";
+      Router::$controller_path = APPPATH . "controllers/{$item->type}s.php";
+      Router::$method = $item->id;
+    }
+  }
+
+  /**
+   * Just like url::file() except that it returns an absolute URI
+   */
+  static function abs_file($path) {
+    return url::base(
+      false, (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') ? 'http' : 'https') . $path;
+  }
+
+  /**
+   * Just like url::site() except that it returns an absolute URI and
+   * doesn't take a protocol parameter.
+   */
+  static function abs_site($path) {
+    return url::site(
+      $path, (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') ? 'http' : 'https');
+  }
+
+  /**
+   * Just like url::current except that it returns an absolute URI
+   */
+  static function abs_current($qs=false) {
+    return self::abs_site(url::current($qs));
+  }
+}
diff --git a/modules/gallery/helpers/access.php b/modules/gallery/helpers/access.php
new file mode 100644
index 00000000..64ce91fa
--- /dev/null
+++ b/modules/gallery/helpers/access.php
@@ -0,0 +1,628 @@
+ tuples.  It would be inefficient to check
+ *   these tuples every time we want to do a lookup, so we use these intents to create an entire
+ *   table of permissions for easy lookup in the Access_Cache_Model.  There's a 1:1 mapping
+ *   between Item_Model and Access_Cache_Model entries.
+ *
+ * o For efficiency, we create columns in Access_Intent_Model and Access_Cache_Model for each of
+ *   the possible Group_Model and Permission_Model combinations.  This may lead to performance
+ *   issues for very large Gallery installs, but for small to medium sized ones (5-10 groups, 5-10
+ *   permissions) it's especially efficient because there's a single field value for each
+ *   group/permission/item combination.
+ *
+ * o For efficiency, we store the cache columns for view permissions directly in the Item_Model.
+ *   This means that we can filter items by group/permission combination without doing any table
+ *   joins making for an especially efficient permission check at the expense of having to
+ *   maintain extra columns for each item.
+ *
+ * o If at any time the Access_Cache_Model becomes invalid, we can rebuild the entire table from
+ *   the Access_Intent_Model
+ */
+class access_Core {
+  const DENY      = 0;
+  const ALLOW     = 1;
+  const UNKNOWN   = 2;
+
+  /**
+   * Does the active user have this permission on this item?
+   *
+   * @param  string     $perm_name
+   * @param  Item_Model $item
+   * @return boolean
+   */
+  static function can($perm_name, $item) {
+    if (!$item->loaded) {
+      return false;
+    }
+
+    if (user::active()->admin) {
+      return true;
+    }
+
+    $resource = $perm_name == "view" ?
+      $item : model_cache::get("access_cache", $item->id, "item_id");
+    foreach (user::group_ids() as $id) {
+      if ($resource->__get("{$perm_name}_$id") === self::ALLOW) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * If the active user does not have this permission, failed with an access::forbidden().
+   *
+   * @param  string     $perm_name
+   * @param  Item_Model $item
+   * @return boolean
+   */
+  static function required($perm_name, $item) {
+    if (!self::can($perm_name, $item)) {
+      self::forbidden();
+    }
+  }
+
+  /**
+   * Does this group have this permission on this item?
+   *
+   * @param  Group_Model $group
+   * @param  string      $perm_name
+   * @param  Item_Model  $item
+   * @return boolean
+   */
+  static function group_can($group, $perm_name, $item) {
+    $resource = $perm_name == "view" ?
+      $item : model_cache::get("access_cache", $item->id, "item_id");
+    return $resource->__get("{$perm_name}_{$group->id}") === self::ALLOW;
+  }
+
+  /**
+   * Return this group's intent for this permission on this item.
+   *
+   * @param  Group_Model $group
+   * @param  string      $perm_name
+   * @param  Item_Model  $item
+   * @return integer     access::ALLOW, access::DENY or null for no intent
+   */
+  static function group_intent($group, $perm_name, $item) {
+    $intent = model_cache::get("access_intent", $item->id, "item_id");
+    return $intent->__get("{$perm_name}_{$group->id}");
+  }
+
+  /**
+   * Is the permission on this item locked by a parent?  If so return the nearest parent that
+   * locks it.
+   *
+   * @param  Group_Model $group
+   * @param  string      $perm_name
+   * @param  Item_Model  $item
+   * @return ORM_Model   item that locks this one
+   */
+  static function locked_by($group, $perm_name, $item) {
+    if ($perm_name != "view") {
+      return null;
+    }
+
+    // For view permissions, if any parent is self::DENY, then those parents lock this one.
+    // Return
+    $lock = ORM::factory("item")
+      ->where("`left` <= $item->left")
+      ->where("`right` >= $item->right")
+      ->where("items.id <> $item->id")
+      ->join("access_intents", "items.id", "access_intents.item_id")
+      ->where("access_intents.view_$group->id", 0)
+      ->orderby("level", "DESC")
+      ->limit(1)
+      ->find();
+
+    if ($lock->loaded) {
+      return $lock;
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Terminate immediately with an HTTP 503 Forbidden response.
+   */
+  static function forbidden() {
+    throw new Exception("@todo FORBIDDEN", 503);
+  }
+
+  /**
+   * Internal method to set a permission
+   *
+   * @param  Group_Model $group
+   * @param  string      $perm_name
+   * @param  Item_Model  $item
+   * @param  boolean     $value
+   */
+  private static function _set(Group_Model $group, $perm_name, $album, $value) {
+    if (get_class($group) != "Group_Model") {
+      throw new Exception("@todo PERMISSIONS_ONLY_WORK_ON_GROUPS");
+    }
+    if (!$album->loaded) {
+      throw new Exception("@todo INVALID_ALBUM $album->id");
+    }
+    if (!$album->is_album()) {
+      throw new Exception("@todo INVALID_ALBUM_TYPE not an album");
+    }
+    $access = model_cache::get("access_intent", $album->id, "item_id");
+    $access->__set("{$perm_name}_{$group->id}", $value);
+    $access->save();
+
+    if ($perm_name == "view") {
+      self::_update_access_view_cache($group, $album);
+    } else {
+      self::_update_access_non_view_cache($group, $perm_name, $album);
+    }
+
+    self::_update_htaccess_files($album, $group, $perm_name, $value);
+  }
+
+  /**
+   * Allow a group to have a permission on an item.
+   *
+   * @param  Group_Model $group
+   * @param  string  $perm_name
+   * @param  Item_Model $item
+   */
+  static function allow($group, $perm_name, $item) {
+    self::_set($group, $perm_name, $item, self::ALLOW);
+  }
+
+  /**
+   * Deny a group the given permission on an item.
+   *
+   * @param  Group_Model $group
+   * @param  string  $perm_name
+   * @param  Item_Model $item
+   */
+  static function deny($group, $perm_name, $item) {
+    self::_set($group, $perm_name, $item, self::DENY);
+  }
+
+  /**
+   * Unset the given permission for this item and use inherited values
+   *
+   * @param  Group_Model $group
+   * @param  string  $perm_name
+   * @param  Item_Model $item
+   */
+  static function reset($group, $perm_name, $item) {
+    if ($item->id == 1) {
+      throw new Exception("@todo CANT_RESET_ROOT_PERMISSION");
+    }
+    self::_set($group, $perm_name, $item, null);
+  }
+
+  /**
+   * Register a permission so that modules can use it.
+   *
+   * @param  string $name           The internal name for for this permission
+   * @param  string $display_name   The internationalized version of the displayable name
+   * @return void
+  */
+  static function register_permission($name, $display_name) {
+    $permission = ORM::factory("permission", $name);
+    if ($permission->loaded) {
+      throw new Exception("@todo PERMISSION_ALREADY_EXISTS $name");
+    }
+    $permission->name = $name;
+    $permission->display_name = $display_name;
+    $permission->save();
+
+    foreach (self::_get_all_groups() as $group) {
+      self::_add_columns($name, $group);
+    }
+  }
+
+  /**
+   * Delete a permission.
+   *
+   * @param  string $perm_name
+   * @return void
+   */
+  static function delete_permission($name) {
+    foreach (self::_get_all_groups() as $group) {
+      self::_drop_columns($name, $group);
+    }
+    $permission = ORM::factory("permission")->where("name", $name)->find();
+    if ($permission->loaded) {
+      $permission->delete();
+    }
+  }
+
+  /**
+   * Add the appropriate columns for a new group
+   *
+   * @param Group_Model $group
+   * @return void
+   */
+  static function add_group($group) {
+    foreach (ORM::factory("permission")->find_all() as $perm) {
+      self::_add_columns($perm->name, $group);
+    }
+  }
+
+  /**
+   * Remove a group's permission columns (usually when it's deleted)
+   *
+   * @param Group_Model $group
+   * @return void
+   */
+  static function delete_group($group) {
+    foreach (ORM::factory("permission")->find_all() as $perm) {
+      self::_drop_columns($perm->name, $group);
+    }
+  }
+
+  /**
+   * Add new access rows when a new item is added.
+   *
+   * @param Item_Model $item
+   * @return void
+   */
+  static function add_item($item) {
+    $access_intent = ORM::factory("access_intent", $item->id);
+    if ($access_intent->loaded) {
+      throw new Exception("@todo ITEM_ALREADY_ADDED $item->id");
+    }
+    $access_intent = ORM::factory("access_intent");
+    $access_intent->item_id = $item->id;
+    $access_intent->save();
+
+    // Create a new access cache entry and copy the parents values.
+    $access_cache = ORM::factory("access_cache");
+    $access_cache->item_id = $item->id;
+    if ($item->id != 1) {
+      $parent_access_cache =
+        ORM::factory("access_cache")->where("item_id", $item->parent()->id)->find();
+      foreach (self::_get_all_groups() as $group) {
+        foreach (ORM::factory("permission")->find_all() as $perm) {
+          $field = "{$perm->name}_{$group->id}";
+          if ($perm->name == "view") {
+            $item->$field = $item->parent()->$field;
+          } else {
+            $access_cache->$field = $parent_access_cache->$field;
+          }
+        }
+      }
+    }
+    $item->save();
+    $access_cache->save();
+  }
+
+  /**
+   * Delete appropriate access rows when an item is deleted.
+   *
+   * @param Item_Model $item
+   * @return void
+   */
+  static function delete_item($item) {
+    ORM::factory("access_intent")->where("item_id", $item->id)->find()->delete();
+    ORM::factory("access_cache")->where("item_id", $item->id)->find()->delete();
+  }
+
+  /**
+   * Verify our Cross Site Request Forgery token is valid, else throw an exception.
+   */
+  static function verify_csrf() {
+    $input = Input::instance();
+    if ($input->post("csrf", $input->get("csrf", null)) !== Session::instance()->get("csrf")) {
+      self::forbidden();
+    }
+  }
+
+  /**
+   * Get the Cross Site Request Forgery token for this session.
+   * @return string
+   */
+  static function csrf_token() {
+    $session = Session::instance();
+    $csrf = $session->get("csrf");
+    if (empty($csrf)) {
+      $csrf = md5(rand());
+      $session->set("csrf", $csrf);
+    }
+    return $csrf;
+  }
+
+  /**
+   * Generate an  element containing the Cross Site Request Forgery token for this session.
+   * @return string
+   */
+  static function csrf_form_field() {
+    return "";
+  }
+
+  /**
+   * Internal method to get all available groups.
+   *
+   * @return ORM_Iterator
+   */
+  private static function _get_all_groups() {
+    // When we build the core package, it's possible that the user module is not installed yet.
+    // This is ok at packaging time, so work around it.
+    if (module::is_active("user")) {
+      return ORM::factory("group")->find_all();
+    } else {
+      return array();
+    }
+  }
+
+  /**
+   * Internal method to  remove Permission/Group columns
+   *
+   * @param  Group_Model $group
+   * @param  string      $perm_name
+   * @return void
+   */
+  private static function _drop_columns($perm_name, $group) {
+    $db = Database::instance();
+    $field = "{$perm_name}_{$group->id}";
+    $cache_table = $perm_name == "view" ? "items" : "access_caches";
+    $db->query("ALTER TABLE {{$cache_table}} DROP `$field`");
+    $db->query("ALTER TABLE {access_intents} DROP `$field`");
+    ORM::factory("access_intent")->clear_cache();
+  }
+
+  /**
+   * Internal method to add Permission/Group columns
+   *
+   * @param  Group_Model $group
+   * @param  string  $perm_name
+   * @return void
+   */
+  private static function _add_columns($perm_name, $group) {
+    $db = Database::instance();
+    $field = "{$perm_name}_{$group->id}";
+    $cache_table = $perm_name == "view" ? "items" : "access_caches";
+    $db->query("ALTER TABLE {{$cache_table}} ADD `$field` SMALLINT NOT NULL DEFAULT 0");
+    $db->query("ALTER TABLE {access_intents} ADD `$field` BOOLEAN DEFAULT NULL");
+    $db->update("access_intents", array($field => 0), array("item_id" => 1));
+    ORM::factory("access_intent")->clear_cache();
+  }
+
+  /**
+   * Update the Access_Cache model based on information from the Access_Intent model for view
+   * permissions only.
+   *
+   * @todo: use database locking
+   *
+   * @param  Group_Model $group
+   * @param  Item_Model $item
+   * @return void
+   */
+  private static function _update_access_view_cache($group, $item) {
+    $access = ORM::factory("access_intent")->where("item_id", $item->id)->find();
+
+    $db = Database::instance();
+    $field = "view_{$group->id}";
+
+    // With view permissions, deny values in the parent can override allow values in the child,
+    // so start from the bottom of the tree and work upwards overlaying negative on top of
+    // positive.
+    //
+    // If the item's intent is ALLOW or DEFAULT, it's possible that some ancestor has specified
+    // DENY and this ALLOW cannot be obeyed.  So in that case, back up the tree and find any
+    // non-DEFAULT and non-ALLOW parent and propagate from there.  If we can't find a matching
+    // item, then its safe to propagate from here.
+    if ($access->$field !== self::DENY) {
+      $tmp_item = ORM::factory("item")
+        ->where("left <", $item->left)
+        ->where("right >", $item->right)
+        ->join("access_intents", "access_intents.item_id", "items.id")
+        ->where("access_intents.$field", self::DENY)
+        ->orderby("left", "DESC")
+        ->limit(1)
+        ->find();
+      if ($tmp_item->loaded) {
+        $item = $tmp_item;
+      }
+    }
+
+    // We will have a problem if we're trying to change a DENY to an ALLOW because the
+    // access_caches table will already contain DENY values and we won't be able to overwrite
+    // them according the rule above.  So mark every permission below this level as UNKNOWN so
+    // that we can tell which permissions have been changed, and which ones need to be updated.
+    $db->update("items", array($field => self::UNKNOWN),
+                array("left >=" => $item->left, "right <=" => $item->right));
+
+    $query = ORM::factory("access_intent")
+      ->select(array("access_intents.$field", "items.left", "items.right", "items.id"))
+      ->join("items", "items.id", "access_intents.item_id")
+      ->where("left >=", $item->left)
+      ->where("right <=", $item->right)
+      ->where("type", "album")
+      ->where("access_intents.$field IS NOT", null)
+      ->orderby("level", "DESC")
+      ->find_all();
+    foreach ($query as $row) {
+      if ($row->$field == self::ALLOW) {
+        // Propagate ALLOW for any row that is still UNKNOWN.
+        $db->update("items", array($field => $row->$field),
+          array($field => self::UNKNOWN, "left >=" => $row->left, "right <=" => $row->right));
+      } else if ($row->$field == self::DENY) {
+        // DENY overwrites everything below it
+        $db->update("items", array($field => $row->$field),
+                    array("left >=" => $row->left, "right <=" => $row->right));
+      }
+    }
+
+    // Finally, if our intent is DEFAULT at this point it means that we were unable to find a
+    // DENY parent in the hierarchy to propagate from.  So we'll still have a UNKNOWN values in
+    // the hierarchy, and all of those are safe to change to ALLOW.
+    $db->update("items", array($field => self::ALLOW),
+                array($field => self::UNKNOWN, "left >=" => $item->left, "right <=" => $item->right));
+  }
+
+  /**
+   * Update the Access_Cache model based on information from the Access_Intent model for non-view
+   * permissions.
+   *
+   * @todo: use database locking
+   *
+   * @param  Group_Model $group
+   * @param  string  $perm_name
+   * @param  Item_Model $item
+   * @return void
+   */
+  private static function _update_access_non_view_cache($group, $perm_name, $item) {
+    $access = ORM::factory("access_intent")->where("item_id", $item->id)->find();
+
+    $db = Database::instance();
+    $field = "{$perm_name}_{$group->id}";
+
+    // If the item's intent is DEFAULT, then we need to back up the chain to find the nearest
+    // parent with an intent and propagate from there.
+    //
+    // @todo To optimize this, we wouldn't need to propagate from the parent, we could just
+    //       propagate from here with the parent's intent.
+    if ($access->$field === null) {
+      $tmp_item = ORM::factory("item")
+        ->join("access_intents", "items.id", "access_intents.item_id")
+        ->where("left <", $item->left)
+        ->where("right >", $item->right)
+        ->where("$field IS NOT", null)
+        ->orderby("left", "DESC")
+        ->limit(1)
+        ->find();
+      if ($tmp_item->loaded) {
+        $item = $tmp_item;
+      }
+    }
+
+    // With non-view permissions, each level can override any permissions that came above it
+    // so start at the top and work downwards, overlaying permissions as we go.
+    $query = ORM::factory("access_intent")
+      ->select(array("access_intents.$field", "items.left", "items.right"))
+      ->join("items", "items.id", "access_intents.item_id")
+      ->where("left >=", $item->left)
+      ->where("right <=", $item->right)
+      ->where("$field IS NOT", null)
+      ->orderby("level", "ASC")
+      ->find_all();
+    foreach  ($query as $row) {
+      $db->query(
+        "UPDATE {access_caches} SET `$field` = {$row->$field} " .
+        "WHERE `item_id` IN " .
+        "  (SELECT `id` FROM {items} " .
+        "  WHERE `left` >= $row->left " .
+        "  AND `right` <= $row->right)");
+    }
+  }
+
+  /**
+   * Maintain .htacccess files to prevent direct access to albums, resizes and thumbnails when we
+   * apply the view and view_full permissions to guest users.
+   */
+  private static function _update_htaccess_files($album, $group, $perm_name, $value) {
+    if ($group->id != 1 || !($perm_name == "view" || $perm_name == "view_full")) {
+      return;
+    }
+
+    $dirs = array($album->file_path());
+    if ($perm_name == "view") {
+      $dirs[] = dirname($album->resize_path());
+      $dirs[] = dirname($album->thumb_path());
+    }
+
+    $base_url = url::site("file_proxy");
+    foreach ($dirs as $dir) {
+      if ($value === self::DENY) {
+        $fp = fopen("$dir/.htaccess", "w+");
+        fwrite($fp, "\n");
+        fwrite($fp, "  RewriteEngine On\n");
+        fwrite($fp, "  RewriteRule (.*) $base_url/\$1 [L]\n");
+        fwrite($fp, "\n");
+        fwrite($fp, "\n");
+        fwrite($fp, "  Order Deny,Allow\n");
+        fwrite($fp, "  Deny from All\n");
+        fwrite($fp, "\n");
+        fclose($fp);
+      } else {
+        @unlink($dir . "/.htaccess");
+      }
+    }
+  }
+
+  static function private_key() {
+    return module::get_var("core", "private_key");
+  }
+
+  /**
+   * Verify that our htaccess based permission system actually works.  Create a temporary
+   * directory containing an .htaccess file that uses mod_rewrite to redirect /verify to
+   * /success.  Then request that url.  If we retrieve it successfully, then our redirects are
+   * working and our permission system works.
+   */
+  static function htaccess_works() {
+    $success_url = url::file("var/tmp/security_test/success");
+
+    @mkdir(VARPATH . "tmp/security_test");
+    if ($fp = @fopen(VARPATH . "tmp/security_test/.htaccess", "w+")) {
+      fwrite($fp, "RewriteEngine On\n");
+      fwrite($fp, "RewriteRule verify $success_url [L]\n");
+      fclose($fp);
+    }
+
+    if ($fp = @fopen(VARPATH . "tmp/security_test/success", "w+")) {
+      fwrite($fp, "success");
+      fclose($fp);
+    }
+
+    list ($response) = remote::do_request(url::abs_file("var/tmp/security_test/verify"));
+    $works = $response == "HTTP/1.1 200 OK";
+    @dir::unlink(VARPATH . "tmp/security_test");
+
+    return $works;
+  }
+}
diff --git a/modules/gallery/helpers/album.php b/modules/gallery/helpers/album.php
new file mode 100644
index 00000000..362b93d0
--- /dev/null
+++ b/modules/gallery/helpers/album.php
@@ -0,0 +1,132 @@
+loaded || !$parent->is_album()) {
+      throw new Exception("@todo INVALID_PARENT");
+    }
+
+    if (strpos($name, "/")) {
+      throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH");
+    }
+
+    // We don't allow trailing periods as a security measure
+    // ref: http://dev.kohanaphp.com/issues/684
+    if (rtrim($name, ".") != $name) {
+      throw new Exception("@todo NAME_CANNOT_END_IN_PERIOD");
+    }
+
+    $album = ORM::factory("item");
+    $album->type = "album";
+    $album->title = $title;
+    $album->description = $description;
+    $album->name = $name;
+    $album->owner_id = $owner_id;
+    $album->thumb_dirty = 1;
+    $album->resize_dirty = 1;
+    $album->rand_key = ((float)mt_rand()) / (float)mt_getrandmax();
+    $album->sort_column = "weight";
+    $album->sort_order = "ASC";
+
+    while (ORM::factory("item")
+           ->where("parent_id", $parent->id)
+           ->where("name", $album->name)
+           ->find()->id) {
+      $album->name = "{$name}-" . rand();
+    }
+
+    $album = $album->add_to_parent($parent);
+    mkdir($album->file_path());
+    mkdir(dirname($album->thumb_path()));
+    mkdir(dirname($album->resize_path()));
+
+    module::event("item_created", $album);
+
+    return $album;
+  }
+
+  static function get_add_form($parent) {
+    $form = new Forge("albums/{$parent->id}", "", "post", array("id" => "gAddAlbumForm"));
+    $group = $form->group("add_album")
+      ->label(t("Add an album to %album_title", array("album_title" => $parent->title)));
+    $group->input("title")->label(t("Title"));
+    $group->textarea("description")->label(t("Description"));
+    $group->input("name")->label(t("Directory Name"))
+      ->callback("item::validate_no_slashes")
+      ->error_messages("no_slashes", t("The directory name can't contain the \"/\" character"));
+    $group->hidden("type")->value("album");
+    $group->submit("")->value(t("Create"));
+    $form->add_rules_from(ORM::factory("item"));
+    return $form;
+  }
+
+  static function get_edit_form($parent) {
+    $form = new Forge("albums/{$parent->id}", "", "post", array("id" => "gEditAlbumForm"));
+    $form->hidden("_method")->value("put");
+    $group = $form->group("edit_album")->label(t("Edit Album"));
+
+    $group->input("title")->label(t("Title"))->value($parent->title);
+    $group->textarea("description")->label(t("Description"))->value($parent->description);
+    if ($parent->id != 1) {
+      $group->input("dirname")->label(t("Directory Name"))->value($parent->name)
+        ->callback("item::validate_no_slashes")
+        ->error_messages("no_slashes", t("The directory name can't contain a \"/\""))
+        ->callback("item::validate_no_trailing_period")
+        ->error_messages("no_trailing_period", t("The directory name can't end in \".\""));
+    }
+
+    $sort_order = $group->group("sort_order", array("id" => "gAlbumSortOrder"))
+      ->label(t("Sort Order"));
+
+    $sort_order->dropdown("column", array("id" => "gAlbumSortColumn"))
+      ->label(t("Sort by"))
+      ->options(array("weight" => t("Default"),
+                      "captured" => t("Capture Date"),
+                      "created" => t("Creation Date"),
+                      "title" => t("Title"),
+                      "updated" => t("Updated Date"),
+                      "view_count" => t("Number of views"),
+                      "rand_key" => t("Random")))
+      ->selected($parent->sort_column);
+    $sort_order->dropdown("direction", array("id" => "gAlbumSortDirection"))
+      ->label(t("Order"))
+      ->options(array("ASC" => t("Ascending"),
+                      "DESC" => t("Descending")))
+      ->selected($parent->sort_order);
+    $group->hidden("type")->value("album");
+    $group->submit("")->value(t("Modify"));
+    $form->add_rules_from(ORM::factory("item"));
+    return $form;
+  }
+}
diff --git a/modules/gallery/helpers/batch.php b/modules/gallery/helpers/batch.php
new file mode 100644
index 00000000..0faa3369
--- /dev/null
+++ b/modules/gallery/helpers/batch.php
@@ -0,0 +1,40 @@
+set("batch_level", $session->get("batch_level", 0) + 1);
+  }
+
+  static function stop() {
+    $session = Session::instance();
+    $batch_level = $session->get("batch_level", 0) - 1;
+    if ($batch_level > 0) {
+      $session->set("batch_level", $batch_level);
+    } else {
+      $session->delete("batch_level");
+      module::event("batch_complete");
+    }
+  }
+
+  static function in_progress() {
+    return Session::instance()->get("batch_level", 0) > 0;
+  }
+}
diff --git a/modules/gallery/helpers/block_manager.php b/modules/gallery/helpers/block_manager.php
new file mode 100644
index 00000000..022626e5
--- /dev/null
+++ b/modules/gallery/helpers/block_manager.php
@@ -0,0 +1,67 @@
+name}_block";
+      if (method_exists($class_name, "get_list")) {
+        foreach (call_user_func(array($class_name, "get_list")) as $id => $title) {
+          $blocks["{$module->name}:$id"] = $title;
+        }
+      }
+    }
+    return $blocks;
+  }
+
+  static function get_html($location) {
+    $active = self::get_active($location);
+    $result = "";
+    foreach ($active as $id => $desc) {
+      if (method_exists("$desc[0]_block", "get")) {
+        $block = call_user_func(array("$desc[0]_block", "get"), $desc[1]);
+        $block->id = $id;
+        $result .= $block;
+      }
+    }
+    return $result;
+  }
+}
diff --git a/modules/gallery/helpers/core.php b/modules/gallery/helpers/core.php
new file mode 100644
index 00000000..63f51f86
--- /dev/null
+++ b/modules/gallery/helpers/core.php
@@ -0,0 +1,52 @@
+admin) {
+      Router::$controller = "maintenance";
+      Router::$controller_path = APPPATH . "controllers/maintenance.php";
+      Router::$method = "index";
+    }
+  }
+
+  /**
+   * This function is called when the Gallery is fully initialized.  We relay it to modules as the
+   * "gallery_ready" event.  Any module that wants to perform an action at the start of every
+   * request should implement the _event::gallery_ready() handler.
+   */
+  static function ready() {
+    module::event("gallery_ready");
+  }
+
+  /**
+   * This function is called right before the Kohana framework shuts down.  We relay it to modules
+   * as the "gallery_shutdown" event.  Any module that wants to perform an action at the start of
+   * every request should implement the _event::gallery_shutdown() handler.
+   */
+  static function shutdown() {
+    module::event("gallery_shutdown");
+  }
+}
\ No newline at end of file
diff --git a/modules/gallery/helpers/core_block.php b/modules/gallery/helpers/core_block.php
new file mode 100644
index 00000000..0e2e9c54
--- /dev/null
+++ b/modules/gallery/helpers/core_block.php
@@ -0,0 +1,100 @@
+ t("Welcome to Gallery 3!"),
+      "photo_stream" => t("Photo Stream"),
+      "log_entries" => t("Log Entries"),
+      "stats" => t("Gallery Stats"),
+      "platform_info" => t("Platform Information"),
+      "project_news" => t("Gallery Project News"));
+  }
+
+  static function get($block_id) {
+    $block = new Block();
+    switch($block_id) {
+    case "welcome":
+      $block->css_id = "gWelcome";
+      $block->title = t("Welcome to Gallery3");
+      $block->content = new View("admin_block_welcome.html");
+      break;
+
+    case "photo_stream":
+      $block->css_id = "gPhotoStream";
+      $block->title = t("Photo Stream");
+      $block->content = new View("admin_block_photo_stream.html");
+      $block->content->photos =
+        ORM::factory("item")->where("type", "photo")->orderby("created", "DESC")->find_all(10);
+      break;
+
+    case "log_entries":
+      $block->css_id = "gLogEntries";
+      $block->title = t("Log Entries");
+      $block->content = new View("admin_block_log_entries.html");
+      $block->content->entries = ORM::factory("log")->orderby("timestamp", "DESC")->find_all(5);
+        break;
+
+    case "stats":
+      $block->css_id = "gStats";
+      $block->title = t("Gallery Stats");
+      $block->content = new View("admin_block_stats.html");
+      $block->content->album_count = ORM::factory("item")->where("type", "album")->count_all();
+      $block->content->photo_count = ORM::factory("item")->where("type", "photo")->count_all();
+      break;
+
+    case "platform_info":
+      $block->css_id = "gPlatform";
+      $block->title = t("Platform Information");
+      $block->content = new View("admin_block_platform.html");
+      if (is_readable("/proc/loadavg")) {
+        $block->content->load_average =
+          join(" ", array_slice(split(" ", array_shift(file("/proc/loadavg"))), 0, 3));
+      } else {
+        $block->content->load_average = t("Unavailable");
+      }
+      break;
+
+    case "project_news":
+      $block->css_id = "gProjectNews";
+      $block->title = t("Gallery Project News");
+      $block->content = new View("admin_block_news.html");
+      $block->content->feed = feed::parse("http://gallery.menalto.com/node/feed", 3);
+      break;
+
+    case "block_adder":
+      $block->css_id = "gBlockAdder";
+      $block->title = t("Dashboard Content");
+      $block->content = self::get_add_block_form();
+    }
+
+    return $block;
+  }
+
+  static function get_add_block_form() {
+    $form = new Forge("admin/dashboard/add_block", "", "post",
+                      array("id" => "gAddDashboardBlockForm"));
+    $group = $form->group("add_block")->label(t("Add Block"));
+    $group->dropdown("id")->label("Available Blocks")->options(block_manager::get_available());
+    $group->submit("center")->value(t("Add to center"));
+    $group->submit("sidebar")->value(t("Add to sidebar"));
+    return $form;
+  }
+}
\ No newline at end of file
diff --git a/modules/gallery/helpers/core_event.php b/modules/gallery/helpers/core_event.php
new file mode 100644
index 00000000..bbb53cc9
--- /dev/null
+++ b/modules/gallery/helpers/core_event.php
@@ -0,0 +1,46 @@
+admin && module::get_var("core", "choose_default_tookit", null)) {
+      graphics::choose_default_toolkit();
+      module::clear_var("core", "choose_default_tookit");
+    }
+  }
+}
diff --git a/modules/gallery/helpers/core_installer.php b/modules/gallery/helpers/core_installer.php
new file mode 100644
index 00000000..cffcbedb
--- /dev/null
+++ b/modules/gallery/helpers/core_installer.php
@@ -0,0 +1,278 @@
+query("CREATE TABLE {access_caches} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `item_id` int(9),
+                   PRIMARY KEY (`id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {access_intents} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `item_id` int(9),
+                   PRIMARY KEY (`id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {graphics_rules} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `active` BOOLEAN default 0,
+                   `args` varchar(255) default NULL,
+                   `module_name` varchar(64) NOT NULL,
+                   `operation` varchar(64) NOT NULL,
+                   `priority` int(9) NOT NULL,
+                   `target`  varchar(32) NOT NULL,
+                   PRIMARY KEY (`id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {items} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `album_cover_item_id` int(9) default NULL,
+                   `captured` int(9) default NULL,
+                   `created` int(9) default NULL,
+                   `description` varchar(2048) default NULL,
+                   `height` int(9) default NULL,
+                   `left` int(9) NOT NULL,
+                   `level` int(9) NOT NULL,
+                   `mime_type` varchar(64) default NULL,
+                   `name` varchar(255) default NULL,
+                   `owner_id` int(9) default NULL,
+                   `parent_id` int(9) NOT NULL,
+                   `rand_key` float default NULL,
+                   `relative_path_cache` varchar(255) default NULL,
+                   `resize_dirty` boolean default 1,
+                   `resize_height` int(9) default NULL,
+                   `resize_width` int(9) default NULL,
+                   `right` int(9) NOT NULL,
+                   `sort_column` varchar(64) default NULL,
+                   `sort_order` char(4) default 'ASC',
+                   `thumb_dirty` boolean default 1,
+                   `thumb_height` int(9) default NULL,
+                   `thumb_width` int(9) default NULL,
+                   `title` varchar(255) default NULL,
+                   `type` varchar(32) NOT NULL,
+                   `updated` int(9) default NULL,
+                   `view_count` int(9) default 0,
+                   `weight` int(9) NOT NULL default 0,
+                   `width` int(9) default NULL,
+                   PRIMARY KEY (`id`),
+                   KEY `parent_id` (`parent_id`),
+                   KEY `type` (`type`),
+                   KEY `random` (`rand_key`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {logs} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `category` varchar(64) default NULL,
+                   `html` varchar(255) default NULL,
+                   `message` text default NULL,
+                   `referer` varchar(255) default NULL,
+                   `severity` int(9) default 0,
+                   `timestamp` int(9) default 0,
+                   `url` varchar(255) default NULL,
+                   `user_id` int(9) default 0,
+                   PRIMARY KEY (`id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {messages} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `key` varchar(255) default NULL,
+                   `severity` varchar(32) default NULL,
+                   `value` varchar(255) default NULL,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`key`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {modules} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `active` BOOLEAN default 0,
+                   `name` varchar(64) default NULL,
+                   `version` int(9) default NULL,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`name`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {themes} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `name` varchar(64) default NULL,
+                   `version` int(9) default NULL,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`name`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {permissions} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `display_name` varchar(64) default NULL,
+                   `name` varchar(64) default NULL,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`name`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {incoming_translations} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `key` char(32) NOT NULL,
+                   `locale` char(10) NOT NULL,
+                   `message` text NOT NULL,
+                   `revision` int(9) DEFAULT NULL,
+                   `translation` text,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`key`, `locale`),
+                   KEY `locale_key` (`locale`, `key`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {outgoing_translations} (
+                   `id` int(9) NOT NULL auto_increment,
+                   `base_revision` int(9) DEFAULT NULL,
+                   `key` char(32) NOT NULL,
+                   `locale` char(10) NOT NULL,
+                   `message` text NOT NULL,
+                   `translation` text,
+                   PRIMARY KEY (`id`),
+                   UNIQUE KEY(`key`, `locale`),
+                   KEY `locale_key` (`locale`, `key`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {sessions} (
+                  `session_id` varchar(127) NOT NULL,
+                  `data` text NOT NULL,
+                  `last_activity` int(10) UNSIGNED NOT NULL,
+                  PRIMARY KEY (`session_id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {tasks} (
+                  `id` int(9) NOT NULL auto_increment,
+                  `callback` varchar(128) default NULL,
+                  `context` text NOT NULL,
+                  `done` boolean default 0,
+                  `name` varchar(128) default NULL,
+                  `owner_id` int(9) default NULL,
+                  `percent_complete` int(9) default 0,
+                  `state` varchar(32) default NULL,
+                  `status` varchar(255) default NULL,
+                  `updated` int(9) default NULL,
+                  PRIMARY KEY (`id`),
+                  KEY (`owner_id`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      $db->query("CREATE TABLE {vars} (
+                  `id` int(9) NOT NULL auto_increment,
+                  `module_name` varchar(64) NOT NULL,
+                  `name` varchar(64) NOT NULL,
+                  `value` text,
+                  PRIMARY KEY (`id`),
+                  UNIQUE KEY(`module_name`, `name`))
+                 ENGINE=InnoDB DEFAULT CHARSET=utf8;");
+
+      foreach (array("albums", "logs", "modules", "resizes", "thumbs", "tmp", "uploads") as $dir) {
+        @mkdir(VARPATH . $dir);
+      }
+
+      access::register_permission("view", "View");
+      access::register_permission("view_full", "View Full Size");
+      access::register_permission("edit", "Edit");
+      access::register_permission("add", "Add");
+
+      $root = ORM::factory("item");
+      $root->type = "album";
+      $root->title = "Gallery";
+      $root->description = "";
+      $root->left = 1;
+      $root->right = 2;
+      $root->parent_id = 0;
+      $root->level = 1;
+      $root->thumb_dirty = 1;
+      $root->resize_dirty = 1;
+      $root->sort_column = "weight";
+      $root->sort_order = "ASC";
+      $root->save();
+      access::add_item($root);
+
+      module::set_var("core", "active_site_theme", "default");
+      module::set_var("core", "active_admin_theme", "admin_default");
+      module::set_var("core", "page_size", 9);
+      module::set_var("core", "thumb_size", 200);
+      module::set_var("core", "resize_size", 640);
+      module::set_var("core", "default_locale", "en_US");
+      module::set_var("core", "image_quality", 75);
+
+      // Add rules for generating our thumbnails and resizes
+      graphics::add_rule(
+        "core", "thumb", "resize",
+        array("width" => 200, "height" => 200, "master" => Image::AUTO),
+        100);
+      graphics::add_rule(
+        "core", "resize", "resize",
+        array("width" => 640, "height" => 480, "master" => Image::AUTO),
+        100);
+
+      // Instantiate default themes (site and admin)
+      foreach (array("default", "admin_default") as $theme_name) {
+        $theme_info = new ArrayObject(parse_ini_file(THEMEPATH . $theme_name . "/theme.info"),
+                                      ArrayObject::ARRAY_AS_PROPS);
+        $theme = ORM::factory("theme");
+        $theme->name = $theme_name;
+        $theme->version = $theme_info->version;
+        $theme->save();
+      }
+
+      block_manager::add("dashboard_sidebar", "core", "block_adder");
+      block_manager::add("dashboard_sidebar", "core", "stats");
+      block_manager::add("dashboard_sidebar", "core", "platform_info");
+      block_manager::add("dashboard_sidebar", "core", "project_news");
+      block_manager::add("dashboard_center", "core", "welcome");
+      block_manager::add("dashboard_center", "core", "photo_stream");
+      block_manager::add("dashboard_center", "core", "log_entries");
+
+      module::set_version("core", 1);
+      module::set_var("core", "version", "3.0 pre-beta svn");
+      module::set_var("core", "choose_default_tookit", 1);
+    }
+  }
+
+  static function uninstall() {
+    $db = Database::instance();
+    $db->query("DROP TABLE IF EXISTS {access_caches}");
+    $db->query("DROP TABLE IF EXISTS {access_intents}");
+    $db->query("DROP TABLE IF EXISTS {graphics_rules}");
+    $db->query("DROP TABLE IF EXISTS {items}");
+    $db->query("DROP TABLE IF EXISTS {logs}");
+    $db->query("DROP TABLE IF EXISTS {messages}");
+    $db->query("DROP TABLE IF EXISTS {modules}");
+    $db->query("DROP TABLE IF EXISTS {themes}");
+    $db->query("DROP TABLE IF EXISTS {incoming_translations}");
+    $db->query("DROP TABLE IF EXISTS {outgoing_translations}");
+    $db->query("DROP TABLE IF EXISTS {permissions}");
+    $db->query("DROP TABLE IF EXISTS {sessions}");
+    $db->query("DROP TABLE IF EXISTS {tasks}");
+    $db->query("DROP TABLE IF EXISTS {vars}");
+    foreach (array("albums", "resizes", "thumbs", "uploads",
+                   "modules", "logs", "database.php") as $entry) {
+      system("/bin/rm -rf " . VARPATH . $entry);
+    }
+  }
+}
diff --git a/modules/gallery/helpers/core_menu.php b/modules/gallery/helpers/core_menu.php
new file mode 100644
index 00000000..eb208560
--- /dev/null
+++ b/modules/gallery/helpers/core_menu.php
@@ -0,0 +1,162 @@
+admin) {
+      $menu->append($scaffold_menu = Menu::factory("submenu")
+                    ->id("scaffold")
+                    ->label("Scaffold"));
+      $scaffold_menu->append(Menu::factory("link")
+                             ->id("scaffold_home")
+                             ->label("Dashboard")
+                             ->url(url::site("scaffold")));
+    }
+
+    $menu->append(Menu::factory("link")
+                  ->id("home")
+                  ->label(t("Home"))
+                  ->url(url::site("albums/1")));
+
+    $item = $theme->item();
+
+    if (user::active()->admin || ($item && access::can("edit", $item))) {
+      $menu->append($options_menu = Menu::factory("submenu")
+                    ->id("options_menu")
+                    ->label(t("Options")));
+
+      if ($item && access::can("edit", $item)) {
+        $options_menu
+          ->append(Menu::factory("dialog")
+                   ->id("edit_item")
+                   ->label($item->is_album() ? t("Edit album") : t("Edit photo"))
+                   ->url(url::site("form/edit/{$item->type}s/$item->id")));
+
+        // @todo Move album options menu to the album quick edit pane
+        // @todo Create resized item quick edit pane menu
+        if ($item->is_album()) {
+          $options_menu
+            ->append(Menu::factory("dialog")
+                     ->id("add_item")
+                     ->label(t("Add a photo"))
+                     ->url(url::site("simple_uploader/app/$item->id")))
+            ->append(Menu::factory("dialog")
+                     ->id("add_album")
+                     ->label(t("Add an album"))
+                     ->url(url::site("form/add/albums/$item->id?type=album")))
+            ->append(Menu::factory("dialog")
+                     ->id("edit_permissions")
+                     ->label(t("Edit permissions"))
+                     ->url(url::site("permissions/browse/$item->id")));
+        }
+      }
+    }
+
+    if (user::active()->admin) {
+      $menu->append($admin_menu = Menu::factory("submenu")
+                    ->id("admin_menu")
+                    ->label(t("Admin")));
+      self::admin($admin_menu, $theme);
+      foreach (module::active() as $module) {
+        if ($module->name == "core") {
+          continue;
+        }
+        $class = "{$module->name}_menu";
+        if (method_exists($class, "admin")) {
+          call_user_func_array(array($class, "admin"), array(&$admin_menu, $theme));
+        }
+      }
+    }
+  }
+
+  static function album($menu, $theme) {
+  }
+
+  static function photo($menu, $theme) {
+    if (access::can("view_full", $theme->item())) {
+      $menu
+        ->append(Menu::factory("link")
+               ->id("fullsize")
+               ->label(t("View full size"))
+               ->url("#")
+               ->css_class("gFullSizeLink"));
+    }
+    $menu
+      ->append(Menu::factory("link")
+               ->id("album")
+               ->label(t("Return to album"))
+               ->url($theme->item()->parent()->url("show={$theme->item->id}"))
+               ->css_id("gAlbumLink"));
+  }
+
+  static function admin($menu, $theme) {
+    $menu
+      ->append(Menu::factory("link")
+               ->id("dashboard")
+               ->label(t("Dashboard"))
+               ->url(url::site("admin")))
+      ->append(Menu::factory("submenu")
+               ->id("settings_menu")
+               ->label(t("Settings"))
+               ->append(Menu::factory("link")
+                        ->id("graphics_toolkits")
+                        ->label(t("Graphics"))
+                        ->url(url::site("admin/graphics")))
+               ->append(Menu::factory("link")
+                        ->id("languages")
+                        ->label(t("Languages"))
+                        ->url(url::site("admin/languages")))
+               ->append(Menu::factory("link")
+                        ->id("l10n_mode")
+                        ->label(Session::instance()->get("l10n_mode", false)
+                                ? t("Stop translating") : t("Start translating"))
+                        ->url(url::site("l10n_client/toggle_l10n_mode?csrf=" .
+                                        access::csrf_token())))
+               ->append(Menu::factory("link")
+                        ->id("advanced")
+                        ->label("Advanced")
+                        ->url(url::site("admin/advanced_settings"))))
+      ->append(Menu::factory("link")
+               ->id("modules")
+               ->label(t("Modules"))
+               ->url(url::site("admin/modules")))
+      ->append(Menu::factory("submenu")
+               ->id("content_menu")
+               ->label(t("Content")))
+      ->append(Menu::factory("submenu")
+               ->id("appearance_menu")
+               ->label(t("Appearance"))
+               ->append(Menu::factory("link")
+                        ->id("themes")
+                        ->label(t("Theme Choice"))
+                        ->url(url::site("admin/themes")))
+               ->append(Menu::factory("link")
+                        ->id("theme_details")
+                        ->label(t("Theme Options"))
+                        ->url(url::site("admin/theme_details"))))
+      ->append(Menu::factory("link")
+               ->id("maintenance")
+               ->label(t("Maintenance"))
+               ->url(url::site("admin/maintenance")))
+      ->append(Menu::factory("submenu")
+               ->id("statistics_menu")
+               ->label(t("Statistics"))
+               ->url("#"));
+  }
+}
diff --git a/modules/gallery/helpers/core_search.php b/modules/gallery/helpers/core_search.php
new file mode 100644
index 00000000..9957a493
--- /dev/null
+++ b/modules/gallery/helpers/core_search.php
@@ -0,0 +1,24 @@
+description, $item->name, $item->title));
+  }
+}
diff --git a/modules/gallery/helpers/core_task.php b/modules/gallery/helpers/core_task.php
new file mode 100644
index 00000000..e078192c
--- /dev/null
+++ b/modules/gallery/helpers/core_task.php
@@ -0,0 +1,166 @@
+count();
+    $tasks = array();
+    $tasks[] = Task_Definition::factory()
+                 ->callback("core_task::rebuild_dirty_images")
+                 ->name(t("Rebuild Images"))
+                 ->description($dirty_count ?
+                               t2("You have one out of date photo",
+                                  "You have %count out of date photos",
+                                  $dirty_count)
+                               : t("All your photos are up to date"))
+      ->severity($dirty_count ? log::WARNING : log::SUCCESS);
+
+    $tasks[] = Task_Definition::factory()
+                 ->callback("core_task::update_l10n")
+                 ->name(t("Update translations"))
+                 ->description(t("Download new and updated translated strings"))
+      ->severity(log::SUCCESS);
+
+    return $tasks;
+  }
+
+  /**
+   * Task that rebuilds all dirty images.
+   * @param Task_Model the task
+   */
+  static function rebuild_dirty_images($task) {
+    $result = graphics::find_dirty_images_query();
+    $remaining = $result->count();
+    $completed = $task->get("completed", 0);
+
+    $i = 0;
+    foreach ($result as $row) {
+      $item = ORM::factory("item", $row->id);
+      if ($item->loaded) {
+        graphics::generate($item);
+      }
+
+      $completed++;
+      $remaining--;
+
+      if (++$i == 2) {
+        break;
+      }
+    }
+
+    $task->status = t2("Updated: 1 image. Total: %total_count.",
+                       "Updated: %count images. Total: %total_count.",
+                       $completed,
+                       array("total_count" => ($remaining + $completed)));
+
+    if ($completed + $remaining > 0) {
+      $task->percent_complete = (int)(100 * $completed / ($completed + $remaining));
+    } else {
+      $task->percent_complete = 100;
+    }
+
+    $task->set("completed", $completed);
+    if ($remaining == 0) {
+      $task->done = true;
+      $task->state = "success";
+      site_status::clear("graphics_dirty");
+    }
+  }
+
+  static function update_l10n(&$task) {
+    $start = microtime(true);
+    $dirs = $task->get("dirs");
+    $files = $task->get("files");
+    $cache = $task->get("cache", array());
+    $i = 0;
+
+    switch ($task->get("mode", "init")) {
+    case "init":  // 0%
+      $dirs = array("core", "modules", "themes", "installer");
+      $files = array();
+      $task->set("mode", "find_files");
+      $task->status = t("Finding files");
+      break;
+
+    case "find_files":  // 0% - 10%
+      while (($dir = array_pop($dirs)) && microtime(true) - $start < 0.5) {
+        if (basename($dir) == "tests") {
+          continue;
+        }
+
+        foreach (glob(DOCROOT . "$dir/*") as $path) {
+          $relative_path = str_replace(DOCROOT, "", $path);
+          if (is_dir($path)) {
+            $dirs[] = $relative_path;
+          } else {
+            $files[] = $relative_path;
+          }
+        }
+      }
+
+      $task->status = t2("Finding files: found 1 file",
+                         "Finding files: found %count files", count($files));
+
+      if (!$dirs) {
+        $task->set("mode", "scan_files");
+        $task->set("total_files", count($files));
+        $task->status = t("Scanning files");
+        $task->percent_complete = 10;
+      }
+      break;
+
+    case "scan_files": // 10% - 90%
+      while (($file = array_pop($files)) && microtime(true) - $start < 0.5) {
+        $file = DOCROOT . $file;
+        switch (pathinfo($file, PATHINFO_EXTENSION)) {
+        case "php":
+          l10n_scanner::scan_php_file($file, $cache);
+          break;
+
+        case "info":
+          l10n_scanner::scan_info_file($file, $cache);
+          break;
+        }
+      }
+
+      $total_files = $task->get("total_files");
+      $task->status = t2("Scanning files: scanned 1 file",
+                         "Scanning files: scanned %count files", $total_files - count($files));
+
+      $task->percent_complete = 10 + 80 * ($total_files - count($files)) / $total_files;
+      if (empty($files)) {
+        $task->set("mode", "fetch_updates");
+        $task->status = t("Fetching updates");
+        $task->percent_complete = 90;
+      }
+      break;
+
+    case "fetch_updates":  // 90% - 100%
+      l10n_client::fetch_updates();
+      $task->done = true;
+      $task->state = "success";
+      $task->status = t("Translations installed/updated");
+      $task->percent_complete = 100;
+    }
+
+    $task->set("files", $files);
+    $task->set("dirs", $dirs);
+    $task->set("cache", $cache);
+  }
+}
\ No newline at end of file
diff --git a/modules/gallery/helpers/core_theme.php b/modules/gallery/helpers/core_theme.php
new file mode 100644
index 00000000..28f544a1
--- /dev/null
+++ b/modules/gallery/helpers/core_theme.php
@@ -0,0 +1,137 @@
+get("debug")) {
+      $buf .= "";
+    }
+    if (($theme->page_type == "album" || $theme->page_type == "photo")
+        && access::can("edit", $theme->item())) {
+      $buf .= "";
+      $buf .= html::script("core/js/quick.js");
+    }
+    if ($theme->page_type == "photo" && access::can("view_full", $theme->item())) {
+      $buf .= "";
+      $buf .= html::script("core/js/fullsize.js");
+    }
+
+    if ($session->get("l10n_mode", false)) {
+      $buf .= "";
+      $buf .= html::script("lib/jquery.cookie.js");
+      $buf .= html::script("core/js/l10n_client.js");
+    }
+
+    return $buf;
+  }
+
+  static function resize_top($theme, $item) {
+    if (access::can("edit", $item)) {
+      $edit_link = url::site("quick/pane/$item->id?page_type=photo");
+      return "
"; + } + } + + static function resize_bottom($theme, $item) { + if (access::can("edit", $item)) { + return "
"; + } + } + + static function thumb_top($theme, $child) { + if (access::can("edit", $child)) { + $edit_link = url::site("quick/pane/$child->id?page_type=album"); + return "
"; + } + } + + static function thumb_bottom($theme, $child) { + if (access::can("edit", $child)) { + return "
"; + } + } + + static function admin_head($theme) { + $session = Session::instance(); + $buf = ""; + if ($session->get("debug")) { + $buf .= ""; + } + + if ($session->get("l10n_mode", false)) { + $buf .= ""; + $buf .= html::script("lib/jquery.cookie.js"); + $buf .= html::script("core/js/l10n_client.js"); + } + + return $buf; + } + + static function page_bottom($theme) { + $session = Session::instance(); + if ($session->get("profiler", false)) { + $profiler = new Profiler(); + $profiler->render(); + } + if ($session->get("l10n_mode", false)) { + return L10n_Client_Controller::l10n_form(); + } + + if ($session->get("after_install")) { + $session->delete("after_install"); + return new View("after_install_loader.html"); + } + } + + static function admin_page_bottom($theme) { + $session = Session::instance(); + if ($session->get("profiler", false)) { + $profiler = new Profiler(); + $profiler->render(); + } + if ($session->get("l10n_mode", false)) { + return L10n_Client_Controller::l10n_form(); + } + } + + static function credits() { + return "
  • " . + t("Powered by Gallery %version", + array("url" => "http://gallery.menalto.com", + "version" => module::get_var("core", "version"))) . + "
  • "; + } + + static function admin_credits() { + return core_theme::credits(); + } +} \ No newline at end of file diff --git a/modules/gallery/helpers/dir.php b/modules/gallery/helpers/dir.php new file mode 100644 index 00000000..1717bdd9 --- /dev/null +++ b/modules/gallery/helpers/dir.php @@ -0,0 +1,40 @@ +isDot()) { + unset($resource); + continue; + } else if ($resource->isFile()) { + unlink($resource->getPathName()); + } else if ($resource->isDir()) { + dir::unlink($resource->getRealPath()); + } + unset($resource); + } + return @rmdir($path); + } + return false; + } + + +} diff --git a/modules/gallery/helpers/graphics.php b/modules/gallery/helpers/graphics.php new file mode 100644 index 00000000..805a95c0 --- /dev/null +++ b/modules/gallery/helpers/graphics.php @@ -0,0 +1,387 @@ + 200, "height" => 200, "master" => Image::AUTO), 100); + * + * Specifies that "core" is adding a rule to resize thumbnails down to a max of 200px on + * the longest side. The core module adds default rules at a priority of 100. You can set + * higher and lower priorities to perform operations before or after this fires. + * + * @param string $module_name the module that added the rule + * @param string $target the target for this operation ("thumb" or "resize") + * @param string $operation the name of the operation + * @param array $args arguments to the operation + * @param integer $priority the priority for this rule (lower priorities are run first) + */ + static function add_rule($module_name, $target, $operation, $args, $priority) { + $rule = ORM::factory("graphics_rule"); + $rule->module_name = $module_name; + $rule->target = $target; + $rule->operation = $operation; + $rule->priority = $priority; + $rule->args = serialize($args); + $rule->active = true; + $rule->save(); + + self::mark_dirty($target == "thumb", $target == "resize"); + } + + /** + * Remove any matching graphics rules + * @param string $module_name the module that added the rule + * @param string $target the target for this operation ("thumb" or "resize") + * @param string $operation the name of the operation + */ + static function remove_rule($module_name, $target, $operation) { + ORM::factory("graphics_rule") + ->where("module_name", $module_name) + ->where("target", $target) + ->where("operation", $operation) + ->delete_all(); + + self::mark_dirty($target == "thumb", $target == "resize"); + } + + /** + * Remove all rules for this module + * @param string $module_name + */ + static function remove_rules($module_name) { + $status = Database::instance()->delete("graphics_rules", array("module_name" => $module_name)); + if (count($status)) { + self::mark_dirty(true, true); + } + } + + /** + * Activate the rules for this module, typically done when the module itself is deactivated. + * Note that this does not mark images as dirty so that if you deactivate and reactivate a + * module it won't cause all of your images to suddenly require a rebuild. + */ + static function activate_rules($module_name) { + Database::instance() + ->update("graphics_rules",array("active" => true), array("module_name" => $module_name)); + } + + /** + * Deactivate the rules for this module, typically done when the module itself is deactivated. + * Note that this does not mark images as dirty so that if you deactivate and reactivate a + * module it won't cause all of your images to suddenly require a rebuild. + */ + static function deactivate_rules($module_name) { + Database::instance() + ->update("graphics_rules",array("active" => false), array("module_name" => $module_name)); + } + + /** + * Rebuild the thumb and resize for the given item. + * @param Item_Model $item + */ + static function generate($item) { + if ($item->is_album()) { + if (!$cover = $item->album_cover()) { + 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()) { + $ops["resize"] = $item->resize_path(); + } + + if (empty($ops)) { + return; + } + + try { + foreach ($ops as $target => $output_file) { + if ($input_item->is_movie()) { + // Convert the movie to a JPG first + $output_file = preg_replace("/...$/", "jpg", $output_file); + movie::extract_frame($input_file, $output_file); + $working_file = $output_file; + } else { + $working_file = $input_file; + } + + foreach (ORM::factory("graphics_rule") + ->where("target", $target) + ->where("active", true) + ->orderby("priority", "asc") + ->find_all() as $rule) { + $args = array($working_file, $output_file, unserialize($rule->args)); + call_user_func_array(array("graphics", $rule->operation), $args); + $working_file = $output_file; + } + } + + if (!empty($ops["thumb"])) { + $dims = getimagesize($item->thumb_path()); + $item->thumb_width = $dims[0]; + $item->thumb_height = $dims[1]; + $item->thumb_dirty = 0; + } + + if (!empty($ops["resize"])) { + $dims = getimagesize($item->resize_path()); + $item->resize_width = $dims[0]; + $item->resize_height = $dims[1]; + $item->resize_dirty = 0; + } + $item->save(); + } catch (Kohana_Exception $e) { + // Something went wrong rebuilding the image. Leave it dirty and move on. + // @todo we should handle this better. + Kohana::log("error", "Caught exception rebuilding image: {$item->title}\n" . + $e->getTraceAsString()); + } + } + + /** + * Resize an image. Valid options are width, height and master. Master is one of the Image + * master dimension constants. + * + * @param string $input_file + * @param string $output_file + * @param array $options + */ + static function resize($input_file, $output_file, $options) { + if (!self::$init) { + self::init_toolkit(); + } + + if (filesize($input_file) == 0) { + throw new Exception("@todo MALFORMED_INPUT_FILE"); + } + + $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); + } else { + Image::factory($input_file) + ->resize($options["width"], $options["height"], $options["master"]) + ->quality(module::get_var("core", "image_quality")) + ->save($output_file); + } + } + + /** + * Rotate an image. Valid options are degrees + * + * @param string $input_file + * @param string $output_file + * @param array $options + */ + static function rotate($input_file, $output_file, $options) { + if (!self::$init) { + self::init_toolkit(); + } + + Image::factory($input_file) + ->quality(module::get_var("core", "image_quality")) + ->rotate($options["degrees"]) + ->save($output_file); + } + + /** + * Overlay an image on top of the input file. + * + * Valid options are: file, mime_type, position, transparency_percent, padding + * + * Valid positions: northwest, north, northeast, + * west, center, east, + * southwest, south, southeast + * + * padding is in pixels + * + * @param string $input_file + * @param string $output_file + * @param array $options + */ + static function composite($input_file, $output_file, $options) { + if (!self::$init) { + self::init_toolkit(); + } + + 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; + } + + Image::factory($input_file) + ->composite($options["file"], $x, $y, $options["transparency"]) + ->quality(module::get_var("core", "image_quality")) + ->save($output_file); + } + + /** + * Return a query result that locates all items with dirty images. + * @return Database_Result Query result + */ + static function find_dirty_images_query() { + return Database::instance()->query( + "SELECT `id` FROM {items} " . + "WHERE ((`thumb_dirty` = 1 AND (`type` <> 'album' OR `album_cover_item_id` IS NOT NULL))" . + " OR (`resize_dirty` = 1 AND `type` = 'photo')) " . + " AND `id` != 1"); + } + + /** + * Mark thumbnails and resizes as dirty. They will have to be rebuilt. + */ + static function mark_dirty($thumbs, $resizes) { + if ($thumbs || $resizes) { + $db = Database::instance(); + $fields = array(); + if ($thumbs) { + $fields["thumb_dirty"] = 1; + } + if ($resizes) { + $fields["resize_dirty"] = 1; + } + $db->update("items", $fields, true); + } + + $count = self::find_dirty_images_query()->count(); + if ($count) { + site_status::warning( + t2("One of your photos is out of date. Click here to fix it", + "%count of your photos are out of date. Click here to fix them", + $count, + array("attrs" => sprintf( + 'href="%s" class="gDialogLink"', + url::site("admin/maintenance/start/core_task::rebuild_dirty_images?csrf=__CSRF__")))), + "graphics_dirty"); + } + } + + /** + * Detect which graphics toolkits are available on this system. Return an array of key value + * pairs where the key is one of gd, imagemagick, graphicsmagick and the value is information + * about that toolkit. For GD we return the version string, and for ImageMagick and + * GraphicsMagick we return the path to the directory containing the appropriate binaries. + */ + static function detect_toolkits() { + $gd = function_exists("gd_info") ? gd_info() : array(); + $exec = function_exists("exec"); + if (!isset($gd["GD Version"])) { + $gd["GD Version"] = false; + } + return array("gd" => $gd, + "imagemagick" => $exec ? dirname(exec("which convert")) : false, + "graphicsmagick" => $exec ? dirname(exec("which gm")) : false); + } + + /** + * This needs to be run once, after the initial install, to choose a graphics toolkit. + */ + static function choose_default_toolkit() { + // Detect a graphics toolkit + $toolkits = graphics::detect_toolkits(); + foreach (array("imagemagick", "graphicsmagick", "gd") as $tk) { + if ($toolkits[$tk]) { + module::set_var("core", "graphics_toolkit", $tk); + module::set_var("core", "graphics_toolkit_path", $tk == "gd" ? "" : $toolkits[$tk]); + break; + } + } + if (!module::get_var("core", "graphics_toolkit")) { + site_status::warning( + t("Graphics toolkit missing! Please choose a toolkit", + array("url" => url::site("admin/graphics"))), + "missing_graphics_toolkit"); + } + } + + /** + * Choose which driver the Kohana Image library uses. + */ + static function init_toolkit() { + switch(module::get_var("core", "graphics_toolkit")) { + case "gd": + Kohana::config_set("image.driver", "GD"); + break; + + case "imagemagick": + Kohana::config_set("image.driver", "ImageMagick"); + Kohana::config_set( + "image.params.directory", module::get_var("core", "graphics_toolkit_path")); + break; + + case "graphicsmagick": + Kohana::config_set("image.driver", "GraphicsMagick"); + Kohana::config_set( + "image.params.directory", module::get_var("core", "graphics_toolkit_path")); + break; + } + + self::$init = 1; + } + + /** + * Verify that a specific graphics function is available with the active toolkit. + * @param string $func (eg rotate, resize) + * @return boolean + */ + static function can($func) { + if (module::get_var("core", "graphics_toolkit") == "gd" && + $func == "rotate" && + !function_exists("imagerotate")) { + return false; + } + + return true; + } +} diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php new file mode 100644 index 00000000..7daaf1e1 --- /dev/null +++ b/modules/gallery/helpers/item.php @@ -0,0 +1,105 @@ +parent(); + if ($parent->album_cover_item_id == $source->id) { + if ($parent->children_count() > 1) { + foreach ($parent->children(2) as $child) { + if ($child->id != $source->id) { + $new_cover_item = $child; + break; + } + } + item::make_album_cover($new_cover_item); + } else { + item::remove_album_cover($parent); + } + } + + $source->move_to($target); + + // If the target has no cover item, make this it. + if ($target->album_cover_item_id == null) { + item::make_album_cover($source); + } + } + + static function make_album_cover($item) { + $parent = $item->parent(); + access::required("edit", $parent); + + model_cache::clear("item", $parent->album_cover_item_id); + $parent->album_cover_item_id = $item->is_album() ? $item->album_cover_item_id : $item->id; + $parent->thumb_dirty = 1; + $parent->save(); + graphics::generate($parent); + $grand_parent = $parent->parent(); + if ($grand_parent && $grand_parent->album_cover_item_id == null) { + item::make_album_cover($parent); + } + } + + static function remove_album_cover($album) { + access::required("edit", $album); + @unlink($album->thumb_path()); + + model_cache::clear("item", $album->album_cover_item_id) ; + $album->album_cover_item_id = null; + $album->thumb_width = 0; + $album->thumb_height = 0; + $album->thumb_dirty = 1; + $album->save(); + graphics::generate($album); + } + + static function validate_no_slashes($input) { + if (strpos($input->value, "/") !== false) { + $input->add_error("no_slashes", 1); + } + } + + static function validate_no_trailing_period($input) { + if (rtrim($input->value, ".") !== $input->value) { + $input->add_error("no_trailing_period", 1); + } + } + + static function validate_no_name_conflict($input) { + $itemid = Input::instance()->post("item"); + if (is_array($itemid)) { + $itemid = $itemid[0]; + } + $item = ORM::factory("item") + ->in("id", $itemid) + ->find(); + if (Database::instance() + ->from("items") + ->where("parent_id", $item->parent_id) + ->where("id <>", $item->id) + ->where("name", $input->value) + ->count_records()) { + $input->add_error("conflict", 1); + } + } +} \ No newline at end of file diff --git a/modules/gallery/helpers/l10n_client.php b/modules/gallery/helpers/l10n_client.php new file mode 100644 index 00000000..ec4c5429 --- /dev/null +++ b/modules/gallery/helpers/l10n_client.php @@ -0,0 +1,203 @@ + $version, + "client_token" => self::client_token(), + "signature" => $signature, + "uid" => self::server_uid($api_key))); + if (!remote::success($response_status)) { + return false; + } + return true; + } + + static function fetch_updates() { + $request->locales = array(); + $request->messages = new stdClass(); + + $locales = locale::installed(); + foreach ($locales as $locale => $locale_data) { + $request->locales[] = $locale; + } + + // @todo Batch requests (max request size) + foreach (Database::instance() + ->select("key", "locale", "revision", "translation") + ->from("incoming_translations") + ->get() + ->as_array() as $row) { + if (!isset($request->messages->{$row->key})) { + $request->messages->{$row->key} = 1; + } + if (!empty($row->revision) && !empty($row->translation)) { + if (!is_object($request->messages->{$row->key})) { + $request->messages->{$row->key} = new stdClass(); + } + $request->messages->{$row->key}->{$row->locale} = $row->revision; + } + } + // @todo Include messages from outgoing_translations? + + $request_data = json_encode($request); + $url = self::_server_url() . "?q=translations/fetch"; + list ($response_data, $response_status) = remote::post($url, array("data" => $request_data)); + if (!remote::success($response_status)) { + throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED " . $response_status); + } + if (empty($response_data)) { + log::info("translations", "Translations fetch request resulted in an empty response"); + return; + } + + $response = json_decode($response_data); + + // Response format (JSON payload): + // [{key:, translation: , rev:, locale:}, + // {key:, ...} + // ] + $count = count($response); + log::info("translations", "Installed $count new / updated translation messages"); + + foreach ($response as $message_data) { + // @todo Better input validation + if (empty($message_data->key) || empty($message_data->translation) || + empty($message_data->locale) || empty($message_data->rev)) { + throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED: Invalid response data"); + } + $key = $message_data->key; + $locale = $message_data->locale; + $revision = $message_data->rev; + $translation = serialize(json_decode($message_data->translation)); + + // @todo Should we normalize the incoming_translations table into messages(id, key, message) + // and incoming_translations(id, translation, locale, revision)? Or just allow + // incoming_translations.message to be NULL? + $locale = $message_data->locale; + $entry = ORM::factory("incoming_translation") + ->where(array("key" => $key, "locale" => $locale)) + ->find(); + if (!$entry->loaded) { + // @todo Load a message key -> message (text) dict into memory outside of this loop + $root_entry = ORM::factory("incoming_translation") + ->where(array("key" => $key, "locale" => "root")) + ->find(); + $entry->key = $key; + $entry->message = $root_entry->message; + $entry->locale = $locale; + } + $entry->revision = $revision; + $entry->translation = $translation; + $entry->save(); + } + } + + static function submit_translations() { + // Request format (HTTP POST): + // client_token = + // uid = + // signature = md5(user_api_key($uid, $client_token) . $data . $client_token)) + // data = // JSON payload + // + // {: {message: + // translations: {: , + // : ...}}, + // : {...} + // } + + // @todo Batch requests (max request size) + // @todo include base_revision in submission / how to handle resubmissions / edit fights? + foreach (Database::instance() + ->select("key", "message", "locale", "base_revision", "translation") + ->from("outgoing_translations") + ->get() as $row) { + $key = $row->key; + if (!isset($request->{$key})) { + $request->{$key}->message = json_encode(unserialize($row->message)); + } + $request->{$key}->translations->{$row->locale} = json_encode(unserialize($row->translation)); + } + + // @todo reduce memory consumpotion, e.g. free $request + $request_data = json_encode($request); + $url = self::_server_url() . "?q=translations/submit"; + $signature = self::_sign($request_data); + + list ($response_data, $response_status) = remote::post( + $url, array("data" => $request_data, + "client_token" => self::client_token(), + "signature" => $signature, + "uid" => self::server_uid())); + + if (!remote::success($response_status)) { + throw new Exception("@todo TRANSLATIONS_SUBMISSION_FAILED " . $response_status); + } + if (empty($response_data)) { + return; + } + + $response = json_decode($response_data); + // Response format (JSON payload): + // [{key:, locale:, rev:, status:}, + // {key:, ...} + // ] + + // @todo Move messages out of outgoing into incoming, using new rev? + // @todo show which messages have been rejected / are pending? + } +} \ No newline at end of file diff --git a/modules/gallery/helpers/l10n_scanner.php b/modules/gallery/helpers/l10n_scanner.php new file mode 100644 index 00000000..80b6f01c --- /dev/null +++ b/modules/gallery/helpers/l10n_scanner.php @@ -0,0 +1,154 @@ +select("key") + ->from("incoming_translations") + ->where("locale", "root") + ->get() as $row) { + $cache[$row->key] = true; + } + } + + $key = I18n::get_message_key($message); + if (array_key_exists($key, $cache)) { + return $cache[$key]; + } + + $entry = ORM::factory("incoming_translation", array("key" => $key)); + if (!$entry->loaded) { + $entry->key = $key; + $entry->message = serialize($message); + $entry->locale = "root"; + $entry->save(); + } + } + + static function scan_php_file($file, &$cache) { + $code = file_get_contents($file); + $raw_tokens = token_get_all($code); + unset($code); + + $tokens = array(); + $func_token_list = array("t" => array(), "t2" => array()); + $token_number = 0; + // Filter out HTML / whitespace, and build a lookup for global function calls. + foreach ($raw_tokens as $token) { + if ((!is_array($token)) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) { + if (is_array($token)) { + if ($token[0] == T_STRING && in_array($token[1], array("t", "t2"))) { + $func_token_list[$token[1]][] = $token_number; + } + } + $tokens[] = $token; + $token_number++; + } + } + unset($raw_tokens); + + if (!empty($func_token_list["t"])) { + l10n_scanner::_parse_t_calls($tokens, $func_token_list["t"], $cache); + } + if (!empty($func_token_list["t2"])) { + l10n_scanner::_parse_plural_calls($tokens, $func_token_list["t2"], $cache); + } + } + + static function scan_info_file($file, &$cache) { + $code = file_get_contents($file); + if (preg_match("#name\s*?=\s*(.*?)\ndescription\s*?=\s*(.*)\n#", $code, $matches)) { + unset($matches[0]); + foreach ($matches as $string) { + l10n_scanner::process_message($string, $cache); + } + } + } + + private static function _parse_t_calls(&$tokens, &$call_list, &$cache) { + foreach ($call_list as $index) { + $function_name = $tokens[$index++]; + $parens = $tokens[$index++]; + $first_param = $tokens[$index++]; + $next_token = $tokens[$index]; + + if ($parens == "(") { + if (in_array($next_token, array(")", ",")) + && (is_array($first_param) && ($first_param[0] == T_CONSTANT_ENCAPSED_STRING))) { + $message = self::_escape_quoted_string($first_param[1]); + l10n_scanner::process_message($message, $cache); + } else { + // t() found, but inside is something which is not a string literal. + // @todo Call status callback with error filename/line. + } + } + } + } + + private static function _parse_plural_calls(&$tokens, &$call_list, &$cache) { + foreach ($call_list as $index) { + $function_name = $tokens[$index++]; + $parens = $tokens[$index++]; + $first_param = $tokens[$index++]; + $first_separator = $tokens[$index++]; + $second_param = $tokens[$index++]; + $next_token = $tokens[$index]; + + if ($parens == "(") { + if ($first_separator == "," && $next_token == "," + && is_array($first_param) && $first_param[0] == T_CONSTANT_ENCAPSED_STRING + && is_array($second_param) && $second_param[0] == T_CONSTANT_ENCAPSED_STRING) { + $singular = self::_escape_quoted_string($first_param[1]); + $plural = self::_escape_quoted_string($first_param[1]); + l10n_scanner::process_message(array("one" => $singular, "other" => $plural), $cache); + } else { + // t2() found, but inside is something which is not a string literal. + // @todo Call status callback with error filename/line. + } + } + } + } + + /** + * Escape quotes in a strings depending on the surrounding + * quote type used. + * + * @param $str The strings to escape + */ + private static function _escape_quoted_string($str) { + $quo = substr($str, 0, 1); + $str = substr($str, 1, -1); + if ($quo == '"') { + $str = stripcslashes($str); + } else { + $str = strtr($str, array("\\'" => "'", "\\\\" => "\\")); + } + return addcslashes($str, "\0..\37\\\""); + } +} diff --git a/modules/gallery/helpers/locale.php b/modules/gallery/helpers/locale.php new file mode 100644 index 00000000..b707637f --- /dev/null +++ b/modules/gallery/helpers/locale.php @@ -0,0 +1,119 @@ +$code)) { + $installed[$code] = $available[$code]; + } + } + return $installed; + } + + static function update_installed($locales) { + // Ensure that the default is included... + $default = module::get_var("core", "default_locale"); + $locales = array_merge($locales, array($default)); + + module::set_var("core", "installed_locales", join("|", $locales)); + } + + // @todo Might want to add a localizable language name as well. + private static function _init_language_data() { + $l["af_ZA"] = "Afrikaans"; // Afrikaans + $l["ar_SA"] = "العربية"; // Arabic + $l["bg_BG"] = "Български"; // Bulgarian + $l["ca_ES"] = "Catalan"; // Catalan + $l["cs_CZ"] = "Česky"; // Czech + $l["da_DK"] = "Dansk"; // Danish + $l["de_DE"] = "Deutsch"; // German + $l["el_GR"] = "Greek"; // Greek + $l["en_GB"] = "English (UK)"; // English (UK) + $l["en_US"] = "English (US)"; // English (US) + $l["es_AR"] = "Español (AR)"; // Spanish (AR) + $l["es_ES"] = "Español"; // Spanish (ES) + $l["es_MX"] = "Español (MX)"; // Spanish (MX) + $l["et_EE"] = "Eesti"; // Estonian + $l["eu_ES"] = "Euskara"; // Basque + $l["fa_IR"] = "فارسي"; // Farsi + $l["fi_FI"] = "Suomi"; // Finnish + $l["fr_FR"] = "Français"; // French + $l["ga_IE"] = "Gaeilge"; // Irish + $l["he_IL"] = "עברית"; // Hebrew + $l["hu_HU"] = "Magyar"; // Hungarian + $l["is_IS"] = "Icelandic"; // Icelandic + $l["it_IT"] = "Italiano"; // Italian + $l["ja_JP"] = "日本語"; // Japanese + $l["ko_KR"] = "한국말"; // Korean + $l["lt_LT"] = "Lietuvių"; // Lithuanian + $l["lv_LV"] = "Latviešu"; // Latvian + $l["nl_NL"] = "Nederlands"; // Dutch + $l["no_NO"] = "Norsk bokmål"; // Norwegian + $l["pl_PL"] = "Polski"; // Polish + $l["pt_BR"] = "Português Brasileiro"; // Portuguese (BR) + $l["pt_PT"] = "Português"; // Portuguese (PT) + $l["ro_RO"] = "Română"; // Romanian + $l["ru_RU"] = "Русский"; // Russian + $l["sk_SK"] = "Slovenčina"; // Slovak + $l["sl_SI"] = "Slovenščina"; // Slovenian + $l["sr_CS"] = "Srpski"; // Serbian + $l["sv_SE"] = "Svenska"; // Swedish + $l["tr_TR"] = "Türkçe"; // Turkish + $l["uk_UA"] = "Українська"; // Ukrainian + $l["vi_VN"] = "Tiếng Việt"; // Vietnamese + $l["zh_CN"] = "简体中文"; // Chinese (CN) + $l["zh_TW"] = "繁體中文"; // Chinese (TW) + asort($l, SORT_LOCALE_STRING); + self::$locales = $l; + } + + static function display_name($locale=null) { + if (empty(self::$locales)) { + self::_init_language_data(); + } + $locale or $locale = I18n::instance()->locale(); + + return self::$locales["$locale"]; + } + + static function is_rtl($locale) { + return in_array($locale, array("he_IL", "fa_IR", "ar_SA")); + } +} \ No newline at end of file diff --git a/modules/gallery/helpers/log.php b/modules/gallery/helpers/log.php new file mode 100644 index 00000000..451f985a --- /dev/null +++ b/modules/gallery/helpers/log.php @@ -0,0 +1,108 @@ +category = $category; + $log->message = $message; + $log->severity = $severity; + $log->html = $html; + $log->url = substr(url::abs_current(true), 0, 255); + $log->referer = request::referrer(null); + $log->timestamp = time(); + $log->user_id = user::active()->id; + $log->save(); + } + + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case self::SUCCESS: + return "gSuccess"; + + case self::INFO: + return "gInfo"; + + case self::WARNING: + return "gWarning"; + + case self::ERROR: + return "gError"; + } + } +} diff --git a/modules/gallery/helpers/message.php b/modules/gallery/helpers/message.php new file mode 100644 index 00000000..af3b96cc --- /dev/null +++ b/modules/gallery/helpers/message.php @@ -0,0 +1,108 @@ +get("messages"); + $status[] = array($msg, $severity); + $session->set("messages", $status); + } + + /** + * Get any pending messages. There are two types of messages, transient and permanent. + * Permanent messages are used to let the admin know that there are pending administrative + * issues that need to be resolved. Transient ones are only displayed once. + * @return html text + */ + static function get() { + $buf = array(); + + $messages = Session::instance()->get_once("messages", array()); + foreach ($messages as $msg) { + $buf[] = "
  • $msg[0]
  • "; + } + if ($buf) { + return "
      " . implode("", $buf) . "
    "; + } + } + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case self::SUCCESS: + return "gSuccess"; + + case self::INFO: + return "gInfo"; + + case self::WARNING: + return "gWarning"; + + case self::ERROR: + return "gError"; + } + } +} diff --git a/modules/gallery/helpers/model_cache.php b/modules/gallery/helpers/model_cache.php new file mode 100644 index 00000000..2649fdbd --- /dev/null +++ b/modules/gallery/helpers/model_cache.php @@ -0,0 +1,46 @@ +$model_name->$field_name->$id)) { + $model = ORM::factory($model_name)->where($field_name, $id)->find(); + if (!$model->loaded) { + throw new Exception("@todo MISSING_MODEL $model_name:$id"); + } + self::$cache->$model_name->$field_name->$id = $model; + } + + return self::$cache->$model_name->$field_name->$id; + } + + static function clear($model_name, $id, $field_name="id") { + if (!empty(self::$cache->$model_name->$field_name->$id)) { + unset(self::$cache->$model_name->$field_name->$id); + } + } + + static function set($model) { + self::$cache->{$model->object_name} + ->{$model->primary_key} + ->{$model->{$model->primary_key}} = $model; + } +} diff --git a/modules/gallery/helpers/module.php b/modules/gallery/helpers/module.php new file mode 100644 index 00000000..a48c89ed --- /dev/null +++ b/modules/gallery/helpers/module.php @@ -0,0 +1,357 @@ +loaded) { + $module->name = $module_name; + $module->active = $module_name == "core"; // only core is active by default + } + $module->version = 1; + $module->save(); + Kohana::log("debug", "$module_name: version is now $version"); + } + + /** + * Load the corresponding Module_Model + * @param string $module_name + */ + static function get($module_name) { + // @todo can't easily use model_cache here because it throw an exception on missing models. + return ORM::factory("module", array("name" => $module_name)); + } + + /** + * Check to see if a module is installed + * @param string $module_name + */ + static function is_installed($module_name) { + return array_key_exists($module_name, self::$modules); + } + + /** + * Check to see if a module is active + * @param string $module_name + */ + static function is_active($module_name) { + return array_key_exists($module_name, self::$modules) && + self::$modules[$module_name]->active; + } + + /** + * Return the list of available modules, including uninstalled modules. + */ + static function available() { + $modules = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS); + foreach (array_merge(array("core/module.info"), glob(MODPATH . "*/module.info")) as $file) { + $module_name = basename(dirname($file)); + $modules->$module_name = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS); + $modules->$module_name->installed = self::is_installed($module_name); + $modules->$module_name->active = self::is_active($module_name); + $modules->$module_name->version = self::get_version($module_name); + $modules->$module_name->locked = false; + } + + // Lock certain modules + $modules->core->locked = true; + $modules->user->locked = true; + $modules->ksort(); + + return $modules; + } + + /** + * Return a list of all the active modules in no particular order. + */ + static function active() { + return self::$active; + } + + /** + * Install a module. This will call _installer::install(), which is responsible for + * creating database tables, setting module variables and and calling module::set_version(). + * Note that after installing, the module must be activated before it is available for use. + * @param string $module_name + */ + static function install($module_name) { + $kohana_modules = Kohana::config("core.modules"); + $kohana_modules[] = MODPATH . $module_name; + Kohana::config_set("core.modules", $kohana_modules); + + $installer_class = "{$module_name}_installer"; + if (method_exists($installer_class, "install")) { + call_user_func_array(array($installer_class, "install"), array()); + } + + // Now the module is installed but inactive, so don't leave it in the active path + array_pop($kohana_modules); + Kohana::config_set("core.modules", $kohana_modules); + + log::success( + "module", t("Installed module %module_name", array("module_name" => $module_name))); + } + + /** + * Activate an installed module. This will call _installer::activate() which should take + * any steps to make sure that the module is ready for use. This will also activate any + * existing graphics rules for this module. + * @param string $module_name + */ + static function activate($module_name) { + $kohana_modules = Kohana::config("core.modules"); + $kohana_modules[] = MODPATH . $module_name; + Kohana::config_set("core.modules", $kohana_modules); + + $installer_class = "{$module_name}_installer"; + if (method_exists($installer_class, "activate")) { + call_user_func_array(array($installer_class, "activate"), array()); + } + + $module = self::get($module_name); + if ($module->loaded) { + $module->active = true; + $module->save(); + } + + self::load_modules(); + graphics::activate_rules($module_name); + log::success( + "module", t("Activated module %module_name", array("module_name" => $module_name))); + } + + /** + * Deactivate an installed module. This will call _installer::deactivate() which + * should take any cleanup steps to make sure that the module isn't visible in any way. + * @param string $module_name + */ + static function deactivate($module_name) { + $installer_class = "{$module_name}_installer"; + if (method_exists($installer_class, "deactivate")) { + call_user_func_array(array($installer_class, "deactivate"), array()); + } + + $module = self::get($module_name); + if ($module->loaded) { + $module->active = false; + $module->save(); + } + + self::load_modules(); + graphics::deactivate_rules($module_name); + log::success( + "module", t("Deactivated module %module_name", array("module_name" => $module_name))); + } + + /** + * Uninstall a deactivated module. This will call _installer::uninstall() which should + * take whatever steps necessary to make sure that all traces of a module are gone. + * @param string $module_name + */ + static function uninstall($module_name) { + $installer_class = "{$module_name}_installer"; + if (method_exists($installer_class, "uninstall")) { + call_user_func(array($installer_class, "uninstall")); + } + + graphics::remove_rule($module_name); + $module = self::get($module_name); + if ($module->loaded) { + $module->delete(); + } + + // We could delete the module vars here too, but it's nice to leave them around + // in case the module gets reinstalled. + + self::load_modules(); + log::success( + "module", t("Uninstalled module %module_name", array("module_name" => $module_name))); + } + + /** + * Load the active modules. This is called at bootstrap time. + */ + static function load_modules() { + // Reload module list from the config file since we'll do a refresh after calling install() + $core = Kohana::config_load("core"); + $kohana_modules = $core["modules"]; + $modules = ORM::factory("module")->find_all(); + + self::$modules = array(); + self::$active = array(); + foreach ($modules as $module) { + self::$modules[$module->name] = $module; + if ($module->active) { + self::$active[] = $module; + } + if ($module->name != "core") { + $kohana_modules[] = MODPATH . $module->name; + } + } + Kohana::config_set("core.modules", $kohana_modules); + } + + /** + * Run a specific event on all active modules. + * @param string $name the event name + * @param mixed $data data to pass to each event handler + */ + static function event($name, &$data=null) { + $args = func_get_args(); + array_shift($args); + $function = str_replace(".", "_", $name); + + foreach (self::$modules as $module) { + if (!$module->active) { + continue; + } + + $class = "{$module->name}_event"; + if (method_exists($class, $function)) { + call_user_func_array(array($class, $function), $args); + } + } + } + + /** + * Get a variable from this module + * @param string $module_name + * @param string $name + * @param string $default_value + * @return the value + */ + static function get_var($module_name, $name, $default_value=null) { + // We cache all vars in core._cache so that we can load all vars at once for + // performance. + if (empty(self::$var_cache)) { + $row = Database::instance() + ->select("value") + ->from("vars") + ->where(array("module_name" => "core", "name" => "_cache")) + ->get() + ->current(); + if ($row) { + self::$var_cache = unserialize($row->value); + } else { + // core._cache doesn't exist. Create it now. + foreach (Database::instance() + ->select("module_name", "name", "value") + ->from("vars") + ->orderby("module_name", "name") + ->get() as $row) { + if ($row->module_name == "core" && $row->name == "_cache") { + // This could happen if there's a race condition + continue; + } + self::$var_cache->{$row->module_name}->{$row->name} = $row->value; + } + $cache = ORM::factory("var"); + $cache->module_name = "core"; + $cache->name = "_cache"; + $cache->value = serialize(self::$var_cache); + $cache->save(); + } + } + + if (isset(self::$var_cache->$module_name->$name)) { + return self::$var_cache->$module_name->$name; + } else { + return $default_value; + } + } + + /** + * Store a variable for this module + * @param string $module_name + * @param string $name + * @param string $value + */ + static function set_var($module_name, $name, $value) { + $var = ORM::factory("var") + ->where("module_name", $module_name) + ->where("name", $name) + ->find(); + if (!$var->loaded) { + $var->module_name = $module_name; + $var->name = $name; + } + $var->value = $value; + $var->save(); + + Database::instance()->delete("vars", array("module_name" => "core", "name" => "_cache")); + self::$var_cache = null; + } + + /** + * Increment the value of a variable for this module + * @param string $module_name + * @param string $name + * @param string $increment (optional, default is 1) + */ + static function incr_var($module_name, $name, $increment=1) { + Database::instance()->query( + "UPDATE {vars} SET `value` = `value` + $increment " . + "WHERE `module_name` = '$module_name' " . + "AND `name` = '$name'"); + + Database::instance()->delete("vars", array("module_name" => "core", "name" => "_cache")); + self::$var_cache = null; + } + + /** + * Remove a variable for this module. + * @param string $module_name + * @param string $name + */ + static function clear_var($module_name, $name) { + $var = ORM::factory("var") + ->where("module_name", $module_name) + ->where("name", $name) + ->find(); + if ($var->loaded) { + $var->delete(); + } + + Database::instance()->delete("vars", array("module_name" => "core", "name" => "_cache")); + self::$var_cache = null; + } + + /** + * Return the version of the installed module. + * @param string $module_name + */ + static function get_version($module_name) { + return self::get($module_name)->version; + } +} diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php new file mode 100644 index 00000000..3293d4ac --- /dev/null +++ b/modules/gallery/helpers/movie.php @@ -0,0 +1,153 @@ +loaded || !$parent->is_album()) { + throw new Exception("@todo INVALID_PARENT"); + } + + if (!is_file($filename)) { + throw new Exception("@todo MISSING_MOVIE_FILE"); + } + + if (strpos($name, "/")) { + throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH"); + } + + // We don't allow trailing periods as a security measure + // ref: http://dev.kohanaphp.com/issues/684 + if (rtrim($name, ".") != $name) { + throw new Exception("@todo NAME_CANNOT_END_IN_PERIOD"); + } + + $movie_info = movie::getmoviesize($filename); + + // Force an extension onto the name + $pi = pathinfo($filename); + if (empty($pi["extension"])) { + $pi["extension"] = image_type_to_extension($movie_info[2], false); + $name .= "." . $pi["extension"]; + } + + $movie = ORM::factory("item"); + $movie->type = "movie"; + $movie->title = $title; + $movie->description = $description; + $movie->name = $name; + $movie->owner_id = $owner_id ? $owner_id : user::active(); + $movie->width = $movie_info[0]; + $movie->height = $movie_info[1]; + $movie->mime_type = strtolower($pi["extension"]) == "mp4" ? "video/mp4" : "video/x-flv"; + $movie->thumb_dirty = 1; + $movie->resize_dirty = 1; + $movie->sort_column = "weight"; + $movie->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); + + // Randomize the name if there's a conflict + while (ORM::Factory("item") + ->where("parent_id", $parent->id) + ->where("name", $movie->name) + ->find()->id) { + // @todo Improve this. Random numbers are not user friendly + $movie->name = rand() . "." . $pi["extension"]; + } + + // This saves the photo + $movie->add_to_parent($parent); + + // If the thumb or resize already exists then rename it + if (file_exists($movie->resize_path()) || + file_exists($movie->thumb_path())) { + $movie->name = $pi["filename"] . "-" . rand() . "." . $pi["extension"]; + $movie->save(); + } + + copy($filename, $movie->file_path()); + + module::event("item_created", $movie); + + // Build our thumbnail + graphics::generate($movie); + + // If the parent has no cover item, make this it. + if (access::can("edit", $parent) && $parent->album_cover_item_id == null) { + item::make_album_cover($movie); + } + + return $movie; + } + + static function getmoviesize($filename) { + $ffmpeg = self::find_ffmpeg(); + if (empty($ffmpeg)) { + throw new Exception("@todo MISSING_FFMPEG"); + } + + $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($filename) . " 2>&1"; + $result = `$cmd`; + if (preg_match("/Stream.*?Video:.*?(\d+)x(\d+).*\ +([0-9\.]+) (fps|tb).*/", + $result, $regs)) { + list ($width, $height) = array($regs[1], $regs[2]); + } else { + list ($width, $height) = array(0, 0); + } + return array($width, $height); + } + + static function extract_frame($input_file, $output_file) { + $ffmpeg = self::find_ffmpeg(); + if (empty($ffmpeg)) { + throw new Exception("@todo MISSING_FFMPEG"); + } + + $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($input_file) . + " -an -ss 00:00:03 -an -r 1 -vframes 1" . + " -y -f mjpeg " . escapeshellarg($output_file); + exec($cmd); + } + + static function find_ffmpeg() { + if (!$ffmpeg_path = module::get_var("core", "ffmpeg_path")) { + if (function_exists("exec")) { + $ffmpeg_path = exec("which ffmpeg"); + if ($ffmpeg_path) { + module::set_var("core", "ffmpeg_path", $ffmpeg_path); + } + } + } + return $ffmpeg_path; + } +} diff --git a/modules/gallery/helpers/photo.php b/modules/gallery/helpers/photo.php new file mode 100644 index 00000000..c1c005f5 --- /dev/null +++ b/modules/gallery/helpers/photo.php @@ -0,0 +1,171 @@ +loaded || !$parent->is_album()) { + throw new Exception("@todo INVALID_PARENT"); + } + + if (!is_file($filename)) { + throw new Exception("@todo MISSING_IMAGE_FILE"); + } + + if (strpos($name, "/")) { + throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH"); + } + + // We don't allow trailing periods as a security measure + // ref: http://dev.kohanaphp.com/issues/684 + if (rtrim($name, ".") != $name) { + throw new Exception("@todo NAME_CANNOT_END_IN_PERIOD"); + } + + $image_info = getimagesize($filename); + + // Force an extension onto the name + $pi = pathinfo($filename); + if (empty($pi["extension"])) { + $pi["extension"] = image_type_to_extension($image_info[2], false); + $name .= "." . $pi["extension"]; + } + + $photo = ORM::factory("item"); + $photo->type = "photo"; + $photo->title = $title; + $photo->description = $description; + $photo->name = $name; + $photo->owner_id = $owner_id ? $owner_id : user::active(); + $photo->width = $image_info[0]; + $photo->height = $image_info[1]; + $photo->mime_type = empty($image_info['mime']) ? "application/unknown" : $image_info['mime']; + $photo->thumb_dirty = 1; + $photo->resize_dirty = 1; + $photo->sort_column = "weight"; + $photo->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); + + // Randomize the name if there's a conflict + while (ORM::Factory("item") + ->where("parent_id", $parent->id) + ->where("name", $photo->name) + ->find()->id) { + // @todo Improve this. Random numbers are not user friendly + $photo->name = rand() . "." . $pi["extension"]; + } + + // This saves the photo + $photo->add_to_parent($parent); + + /* + * If the thumb or resize already exists then rename it. We need to do this after the save + * because the resize_path and thumb_path both call relative_path which caches the + * path. Before add_to_parent the relative path will be incorrect. + */ + if (file_exists($photo->resize_path()) || + file_exists($photo->thumb_path())) { + $photo->name = $pi["filename"] . "-" . rand() . "." . $pi["extension"]; + $photo->save(); + } + + copy($filename, $photo->file_path()); + + module::event("item_created", $photo); + + // Build our thumbnail/resizes + graphics::generate($photo); + + // If the parent has no cover item, make this it. + if (access::can("edit", $parent) && $parent->album_cover_item_id == null) { + item::make_album_cover($photo); + } + + return $photo; + } + + static function get_add_form($parent) { + $form = new Forge("albums/{$parent->id}", "", "post", array("id" => "gAddPhotoForm")); + $group = $form->group("add_photo")->label( + t("Add Photo to %album_title", array("album_title" =>$parent->title))); + $group->input("title")->label(t("Title")); + $group->textarea("description")->label(t("Description")); + $group->input("name")->label(t("Filename")); + $group->upload("file")->label(t("File"))->rules("required|allow[jpg,png,gif,flv,mp4]"); + $group->hidden("type")->value("photo"); + $group->submit("")->value(t("Upload")); + $form->add_rules_from(ORM::factory("item")); + return $form; + } + + static function get_edit_form($photo) { + $form = new Forge("photos/$photo->id", "", "post", array("id" => "gEditPhotoForm")); + $form->hidden("_method")->value("put"); + $group = $form->group("edit_photo")->label(t("Edit Photo")); + $group->input("title")->label(t("Title"))->value($photo->title); + $group->textarea("description")->label(t("Description"))->value($photo->description); + $group->input("filename")->label(t("Filename"))->value($photo->name) + ->error_messages("conflict", t("There is already a file with this name")) + ->callback("item::validate_no_slashes") + ->error_messages("no_slashes", t("The photo name can't contain a \"/\"")) + ->callback("item::validate_no_trailing_period") + ->error_messages("no_trailing_period", t("The photo name can't end in \".\"")); + + $group->submit("")->value(t("Modify")); + $form->add_rules_from(ORM::factory("item")); + return $form; + } + + /** + * Return scaled width and height. + * + * @param integer $width + * @param integer $height + * @param integer $max the target size for the largest dimension + * @param string $format the output format using %d placeholders for width and height + */ + static function img_dimensions($width, $height, $max, $format="width=\"%d\" height=\"%d\"") { + if (!$width || !$height) { + return ""; + } + + if ($width > $height) { + $new_width = $max; + $new_height = (int)$max * ($height / $width); + } else { + $new_height = $max; + $new_width = (int)$max * ($width / $height); + } + return sprintf($format, $new_width, $new_height); + } +} diff --git a/modules/gallery/helpers/rest.php b/modules/gallery/helpers/rest.php new file mode 100644 index 00000000..a63b94c8 --- /dev/null +++ b/modules/gallery/helpers/rest.php @@ -0,0 +1,116 @@ +post("_method", $input->get("_method", request::method())))) { + case "put": return "put"; + case "delete": return "delete"; + default: return "post"; + } + } + } + + /** + * Choose an output format based on what the client prefers to accept. + * @return string "html", "xml" or "json" + */ + static function output_format() { + // Pick a format, but let it be overridden. + $input = Input::instance(); + $fmt = $input->get( + "_format", $input->post( + "_format", request::preferred_accept( + array("xhtml", "html", "xml", "json")))); + + // Some browsers (Chrome!) prefer xhtml over html, but we'll normalize this to html for now. + if ($fmt == "xhtml") { + $fmt = "html"; + } + return $fmt; + } + + /** + * Set HTTP response code. + * @param string Use one of the status code constants defined in this class. + */ + static function http_status($status_code) { + header("HTTP/1.1 " . $status_code); + } + + /** + * Set HTTP Location header. + * @param string URL + */ + static function http_location($url) { + header("Location: " . $url); + } + + /** + * Set HTTP Content-Type header. + * @param string content type + */ + static function http_content_type($type) { + header("Content-Type: " . $type); + } +} diff --git a/modules/gallery/helpers/site_status.php b/modules/gallery/helpers/site_status.php new file mode 100644 index 00000000..6d47e565 --- /dev/null +++ b/modules/gallery/helpers/site_status.php @@ -0,0 +1,132 @@ +where("key", $permanent_key) + ->find(); + if (!$message->loaded) { + $message->key = $permanent_key; + } + $message->severity = $severity; + $message->value = $msg; + $message->save(); + } + + /** + * Remove any permanent message by key. + * @param string $permanent_key + */ + static function clear($permanent_key) { + $message = ORM::factory("message")->where("key", $permanent_key)->find(); + if ($message->loaded) { + $message->delete(); + } + } + + /** + * Get any pending messages. There are two types of messages, transient and permanent. + * Permanent messages are used to let the admin know that there are pending administrative + * issues that need to be resolved. Transient ones are only displayed once. + * @return html text + */ + static function get() { + if (!user::active()->admin) { + return; + } + $buf = array(); + foreach (ORM::factory("message")->find_all() as $msg) { + $value = str_replace('__CSRF__', access::csrf_token(), $msg->value); + $buf[] = "
  • severity) . "\">$value
  • "; + } + + if ($buf) { + return "
      " . implode("", $buf) . "
    "; + } + } + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case self::SUCCESS: + return "gSuccess"; + + case self::INFO: + return "gInfo"; + + case self::WARNING: + return "gWarning"; + + case self::ERROR: + return "gError"; + } + } +} diff --git a/modules/gallery/helpers/task.php b/modules/gallery/helpers/task.php new file mode 100644 index 00000000..a8a004ab --- /dev/null +++ b/modules/gallery/helpers/task.php @@ -0,0 +1,83 @@ +name}_task"; + if (method_exists($class_name, "available_tasks")) { + foreach (call_user_func(array($class_name, "available_tasks")) as $task) { + $tasks[$task->callback] = $task; + } + } + } + + return $tasks; + } + + static function create($task_def, $context) { + $task = ORM::factory("task"); + $task->callback = $task_def->callback; + $task->name = $task_def->name; + $task->percent_complete = 0; + $task->status = ""; + $task->state = "started"; + $task->owner_id = user::active()->id; + $task->context = serialize($context); + $task->save(); + + return $task; + } + + static function cancel($task_id) { + $task = ORM::factory("task", $task_id); + if (!$task->loaded) { + throw new Exception("@todo MISSING_TASK"); + } + $task->done = 1; + $task->state = "cancelled"; + $task->save(); + + return $task; + } + + static function remove($task_id) { + $task = ORM::factory("task", $task_id); + if ($task->loaded) { + $task->delete(); + } + } + + static function run($task_id) { + $task = ORM::factory("task", $task_id); + if (!$task->loaded) { + throw new Exception("@todo MISSING_TASK"); + } + + $task->state = "running"; + call_user_func_array($task->callback, array(&$task)); + $task->save(); + + return $task; + } +} \ No newline at end of file diff --git a/modules/gallery/helpers/theme.php b/modules/gallery/helpers/theme.php new file mode 100644 index 00000000..cbe224db --- /dev/null +++ b/modules/gallery/helpers/theme.php @@ -0,0 +1,61 @@ +"gThemeDetailsForm")); + $group = $form->group("edit_theme"); + $group->input("page_size")->label(t("Items per page"))->id("gPageSize") + ->rules("required|valid_digit") + ->value(module::get_var("core", "page_size")); + $group->input("thumb_size")->label(t("Thumbnail size (in pixels)"))->id("gThumbSize") + ->rules("required|valid_digit") + ->value(module::get_var("core", "thumb_size")); + $group->input("resize_size")->label(t("Resized image size (in pixels)"))->id("gResizeSize") + ->rules("required|valid_digit") + ->value(module::get_var("core", "resize_size")); + $group->textarea("header_text")->label(t("Header text"))->id("gHeaderText") + ->value(module::get_var("core", "header_text")); + $group->textarea("footer_text")->label(t("Footer text"))->id("gFooterText") + ->value(module::get_var("core", "footer_text")); + $group->submit("")->value(t("Save")); + return $form; + } +} + diff --git a/modules/gallery/helpers/xml.php b/modules/gallery/helpers/xml.php new file mode 100644 index 00000000..e734e90c --- /dev/null +++ b/modules/gallery/helpers/xml.php @@ -0,0 +1,35 @@ +\n"; + foreach ($array as $key => $value) { + if (is_array($value)) { + $xml .= xml::to_xml($value, array_slice($element_names, 1)); + } else if (is_object($value)) { + $xml .= xml::to_xml($value->as_array(), array_slice($element_names, 1)); + } else { + $xml .= "<$key>$value\n"; + } + } + $xml .= "\n"; + return $xml; + } +} diff --git a/modules/gallery/hooks/init_gallery.php b/modules/gallery/hooks/init_gallery.php new file mode 100644 index 00000000..2c36795a --- /dev/null +++ b/modules/gallery/hooks/init_gallery.php @@ -0,0 +1,44 @@ +post("g3sid", $input->get("g3sid"))) { + $_COOKIE["g3sid"] = $g3sid; +} + +if ($user_agent = $input->post("user_agent", $input->get("user_agent"))) { + Kohana::$user_agent = $user_agent; +} diff --git a/modules/gallery/images/gallery.png b/modules/gallery/images/gallery.png new file mode 100644 index 00000000..ca8e0e95 Binary files /dev/null and b/modules/gallery/images/gallery.png differ diff --git a/modules/gallery/images/gd.png b/modules/gallery/images/gd.png new file mode 100644 index 00000000..b341d71c Binary files /dev/null and b/modules/gallery/images/gd.png differ diff --git a/modules/gallery/images/graphicsmagick.png b/modules/gallery/images/graphicsmagick.png new file mode 100644 index 00000000..3d1d77e9 Binary files /dev/null and b/modules/gallery/images/graphicsmagick.png differ diff --git a/modules/gallery/images/imagemagick.jpg b/modules/gallery/images/imagemagick.jpg new file mode 100644 index 00000000..d83c4509 Binary files /dev/null and b/modules/gallery/images/imagemagick.jpg differ diff --git a/modules/gallery/js/albums_form_add.js b/modules/gallery/js/albums_form_add.js new file mode 100644 index 00000000..06a364f3 --- /dev/null +++ b/modules/gallery/js/albums_form_add.js @@ -0,0 +1,12 @@ +$("#gAddAlbumForm input[name=title]").change( + function() { + $("#gAddAlbumForm input[name=name]").attr( + "value", $("#gAddAlbumForm input[name=title]").attr("value"). + replace(/\s+/g, "_").replace(/\.+$/, "")); + }); +$("#gAddAlbumForm input[name=title]").keyup( + function() { + $("#gAddAlbumForm input[name=name]").attr( + "value", $("#gAddAlbumForm input[name=title]").attr("value"). + replace(/\s+/g, "_").replace(/\.+$/, "")); + }); diff --git a/modules/gallery/js/fullsize.js b/modules/gallery/js/fullsize.js new file mode 100644 index 00000000..7428adb5 --- /dev/null +++ b/modules/gallery/js/fullsize.js @@ -0,0 +1,78 @@ +/** + * @todo Move inline CSS out to external style sheet (theme style sheet) + */ +$(document).ready(function() { + $(".gFullSizeLink").click(function() { + var width = $(document).width(); + var height = $(document).height(); + + $("body").append('
    '); + + var image_size = _auto_fit(fullsize_detail.width, fullsize_detail.height); + + $("body").append('
    ' + + '
    '); + + $("#gFullsize").append(''); + $("#gFullsizeClose").click(function() { + $("#gFullsizeOverlay*").remove(); + $("#gFullsize").remove(); + }); + $(window).resize(function() { + $("#gFullsizeOverlay").width($(document).width()); + $("#gFullsizeOverlay").height($(document).height()); + image_size = _auto_fit(fullsize_detail.width, fullsize_detail.height); + $("#gFullsize").height(image_size.height); + $("#gFullsize").width(image_size.width); + $("#gFullsize").css("top", image_size.top); + $("#gFullsize").css("left", image_size.left); + $("#gFullSizeImage").height(image_size.height); + $("#gFullSizeImage").width(image_size.width); + }); + }); +}); + +/* + * Calculate the size of the image panel based on the size of the image and the size of the + * window. Scale the image so the entire panel fits in the view port. + */ +function _auto_fit(imageWidth, imageHeight) { + // ui-dialog gives a padding of 2 pixels + var windowWidth = $(window).width() - 10; + var windowHeight = $(window).height() - 10; + + /* If the width is greater then scale the image width first */ + if (imageWidth > windowWidth) { + var ratio = windowWidth / imageWidth; + imageWidth *= ratio; + imageHeight *= ratio; + } + /* after scaling the width, check that the height fits */ + if (imageHeight > windowHeight) { + var ratio = windowHeight / imageHeight; + imageWidth *= ratio; + imageHeight *= ratio; + } + + // handle the case where the calculation is almost zero (2.14e-14) + return { + top: ((windowHeight - imageHeight) / 2).toFixed(2), + left: ((windowWidth - imageWidth) / 2).toFixed(2), + width: imageWidth.toFixed(2), + height: imageHeight.toFixed(2) + }; +} diff --git a/modules/gallery/js/l10n_client.js b/modules/gallery/js/l10n_client.js new file mode 100644 index 00000000..f43671f1 --- /dev/null +++ b/modules/gallery/js/l10n_client.js @@ -0,0 +1,195 @@ +// Fork from Drupal's l10n_client module, originally written by: +// G‡bor Hojtsy http://drupal.org/user/4166 (original author) +// Young Hahn / Development Seed - http://developmentseed.org/ (friendly user interface) + +var Gallery = Gallery || { 'behaviors': {} }; + +Gallery.attachBehaviors = function(context) { + context = context || document; + // Execute all of them. + jQuery.each(Gallery.behaviors, + function() { + this(context); + }); +}; + +$(document).ready(function() { + Gallery.attachBehaviors(this); +}); + + +// Store all l10n_client related data + methods in its own object +jQuery.extend(Gallery, { + l10nClient: new (function() { + // Set "selected" string to unselected, i.e. -1 + this.selected = -1; + // Keybindings + this.keys = {'toggle':'ctrl+shift+s', 'clear': 'esc'}; // Keybindings + // Keybinding functions + this.key = function(pressed) { + switch(pressed) { + case 'toggle': + // Grab user-hilighted text & send it into the search filter + userSelection = window.getSelection ? window.getSelection() : document.getSelection ? document.getSelection() : document.selection.createRange().text; + userSelection = String(userSelection); + if(userSelection.length > 0) { + Gallery.l10nClient.filter(userSelection); + Gallery.l10nClient.toggle(1); + $('#l10n-client #gL10nSearch').focus(); + } else { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + if(!$.browser.safari) { + $('#l10n-client #gL10nSearch').focus(); + } + } else { + Gallery.l10nClient.toggle(0); + } + } + break; + case 'clear': + this.filter(false); + break; + } + } + // Toggle the l10nclient + this.toggle = function(state) { + switch(state) { + case 1: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').show(); + $('#l10n-client').height('22em').removeClass('hidden'); + $('#l10n-client .labels .toggle').text('X'); + /* + * This CSS clashes with Gallery's CSS, probably due to + * YUI's grid / floats. + if(!$.browser.msie) { + $('body').css('border-bottom', '22em solid #fff'); + } + */ + $.cookie('Gallery_l10n_client', '1', {expires: 7, path: '/'}); + break; + case 0: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').hide(); + $('#l10n-client').height('2em').addClass('hidden'); + // TODO: Localize this message + $('#l10n-client .labels .toggle').text('Translate Text'); + /* + if(!$.browser.msie) { + $('body').css('border-bottom', '0px'); + } + */ + $.cookie('Gallery_l10n_client', '0', {expires: 7, path: '/'}); + break; + } + } + // Get a string from the DOM tree + this.getString = function(index, type) { + return l10n_client_data[index][type]; + } + // Set a string in the DOM tree + this.setString = function(index, data) { + l10n_client_data[index]['translation'] = data; + } + // Filter the the string list by a search string + this.filter = function(search) { + if(search == false || search == '') { + $('#l10n-client #l10n-search-filter-clear').focus(); + $('#l10n-client-string-select li').show(); + $('#l10n-client #gL10nSearch').val(''); + $('#l10n-client #gL10nSearch').focus(); + } else { + if(search.length > 0) { + $('#l10n-client-string-select li').hide(); + $('#l10n-client-string-select li:contains('+search+')').show(); + $('#l10n-client #gL10nSearch').val(search); + } + } + } + }) +}); + +// Attaches the localization editor behavior to all required fields. +Gallery.behaviors.l10nClient = function(context) { + + switch($.cookie('Gallery_l10n_client')) { + case '1': + Gallery.l10nClient.toggle(1); + break; + default: + Gallery.l10nClient.toggle(0); + break; + } + + // If the selection changes, copy string values to the source and target fields. + // Add class to indicate selected string in list widget. + $('#l10n-client-string-select li').click(function() { + $('#l10n-client-string-select li').removeClass('active'); + $(this).addClass('active'); + var index = $('#l10n-client-string-select li').index(this); + + $('#l10n-client-string-editor .source-text').text(Gallery.l10nClient.getString(index, 'source')); + $("#gL10nClientSaveForm input[name='l10n-message-source']").val(Gallery.l10nClient.getString(index, 'source')); + $('#gL10nClientSaveForm #l10n-edit-target').val(Gallery.l10nClient.getString(index, 'translation')); + + Gallery.l10nClient.selected = index; + }); + + // When l10n_client window is clicked, toggle based on current state. + $('#l10n-client .labels .toggle').click(function() { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + } else { + Gallery.l10nClient.toggle(0); + } + }); + + // Register keybindings using jQuery hotkeys + // TODO: Either remove hotkeys code or add query.hotkeys.js. + if($.hotkeys) { + $.hotkeys.add(Gallery.l10nClient.keys['toggle'], function(){Gallery.l10nClient.key('toggle')}); + $.hotkeys.add(Gallery.l10nClient.keys['clear'], {target:'#l10n-client #gL10nSearch', type:'keyup'}, function(){Gallery.l10nClient.key('clear')}); + } + + // Custom listener for l10n_client livesearch + $('#l10n-client #gL10nSearch').keyup(function(key) { + Gallery.l10nClient.filter($('#l10n-client #gL10nSearch').val()); + }); + + // Clear search + $('#l10n-client #l10n-search-filter-clear').click(function() { + Gallery.l10nClient.filter(false); + return false; + }); + + // Send AJAX POST data on form submit. + $('#gL10nClientSaveForm').ajaxForm({ + dataType: "json", + success: function(data) { + // Store string in local js + Gallery.l10nClient.setString(Gallery.l10nClient.selected, $('#gL10nClientSaveForm #l10n-edit-target').val()); + + // Mark string as translated. + $('#l10n-client-string-select li').eq(Gallery.l10nClient.selected).removeClass('untranslated').removeClass('active').addClass('translated').text($('#gL10nClientSaveForm #l10n-edit-target').val()); + + // Empty input fields. + $('#l10n-client-string-editor .source-text').html(''); + $('#gL10nClientSaveForm #l10n-edit-target').val(''); + $("#gL10nClientSaveForm input[name='l10n-message-source']").val(''); + }, + error: function(xmlhttp) { + // TODO: Localize this message + alert('An HTTP error @status occured (or empty response).'.replace('@status', xmlhttp.status)); + } + }); + + + // Copy source text to translation field on button click. + $('#gL10nClientSaveForm #l10n-edit-copy').click(function() { + $('#gL10nClientSaveForm #l10n-edit-target').val($('#l10n-client-string-editor .source-text').text()); + }); + + // Clear translation field on button click. + $('#gL10nClientSaveForm #l10n-edit-clear').click(function() { + $('#gL10nClientSaveForm #l10n-edit-target').val(''); + }); +}; diff --git a/modules/gallery/js/quick.js b/modules/gallery/js/quick.js new file mode 100644 index 00000000..e7f35cea --- /dev/null +++ b/modules/gallery/js/quick.js @@ -0,0 +1,95 @@ +$(document).ready(function() { + if ($("#gAlbumGrid").length) { + // @todo Add quick edit pane for album (meta, move, permissions, delete) + $(".gItem").hover(show_quick, function() {}); + } + if ($("#gPhoto").length) { + $("#gPhoto").hover(show_quick, function() {}); + } +}); + +var show_quick = function() { + var cont = $(this); + var quick = $(this).find(".gQuick"); + $("#gQuickPane").remove(); + cont.append("
    "); + var img = cont.find(".gThumbnail,.gResize"); + var pos = cont.position(); + $("#gQuickPane").css({ + "position": "absolute", + "top": pos.top, + "left": pos.left, + "text-align": "center", + "width": cont.innerWidth() + 1, + "height": "auto" + }).hide(); + cont.hover(function() {}, hide_quick); + $.get( + quick.attr("href"), + {}, + function(data, textStatus) { + $("#gQuickPane").html(data).slideDown("fast"); + $(".ui-state-default").hover( + function(){ + $(this).addClass("ui-state-hover"); + }, + function(){ + $(this).removeClass("ui-state-hover"); + } + ); + $("#gQuickPane a:not(.options)").click(function(e) { + e.preventDefault(); + if ($(this).attr("id") == "gQuickDelete" && + !confirm($(this).attr("ref"))) { + return; + } + quick_do(cont, $(this), img); + }); + $("#gQuickPane a.options").click(function(e) { + e.preventDefault(); + $("#gQuickPaneOptions").slideToggle("fast"); + }); + } + ); +}; + +var quick_do = function(cont, pane, img) { + if (pane.hasClass("ui-state-disabled")) { + return false; + } + if (pane.hasClass("gDialogLink")) { + openDialog(pane, function() { window.location.reload(); }); + } else { + img.css("opacity", "0.1"); + cont.addClass("gLoadingLarge"); + $.ajax({ + type: "GET", + url: pane.attr("href"), + dataType: "json", + success: function(data) { + img.css("opacity", "1"); + cont.removeClass("gLoadingLarge"); + if (data.src) { + img.attr("width", data.width); + img.attr("height", data.height); + img.attr("src", data.src); + if (data.height > data.width) { + img.css("margin-top", -32); + } else { + img.css("margin-top", 0); + } + } else if (data.location) { + window.location = data.location; + } else if (data.reload) { + window.location.reload(); + } + } + }); + } + return false; +}; + +var hide_quick = function() { + $("#gQuickPane").remove(); +}; + diff --git a/modules/gallery/libraries/Admin_View.php b/modules/gallery/libraries/Admin_View.php new file mode 100644 index 00000000..acc3f8ec --- /dev/null +++ b/modules/gallery/libraries/Admin_View.php @@ -0,0 +1,126 @@ +theme_name = module::get_var("core", "active_admin_theme"); + if (user::active()->admin) { + $this->theme_name = Input::instance()->get("theme", $this->theme_name); + } + $this->sidebar = ""; + $this->set_global("theme", $this); + $this->set_global("user", user::active()); + } + + public function url($path, $absolute_url=false) { + $arg = "themes/{$this->theme_name}/$path"; + return $absolute_url ? url::abs_file($arg) : url::file($arg); + } + + public function display($page_name, $view_class="View") { + return new $view_class($page_name); + } + + public function admin_menu() { + $menu = Menu::factory("root"); + core_menu::admin($menu, $this); + + foreach (module::active() as $module) { + if ($module->name == "core") { + continue; + } + $class = "{$module->name}_menu"; + if (method_exists($class, "admin")) { + call_user_func_array(array($class, "admin"), array(&$menu, $this)); + } + } + + print $menu; + } + + /** + * Print out any site wide status information. + */ + public function site_status() { + return site_status::get(); + } + + /** + * Print out any messages waiting for this user. + */ + public function messages() { + return message::get(); + } + + /** + * Handle all theme functions that insert module content. + */ + public function __call($function, $args) { + switch ($function) { + case "admin_credits"; + case "admin_footer": + case "admin_header_top": + case "admin_header_bottom": + case "admin_page_bottom": + case "admin_page_top": + case "admin_head": + $blocks = array(); + foreach (module::active() as $module) { + $helper_class = "{$module->name}_theme"; + if (method_exists($helper_class, $function)) { + $blocks[] = call_user_func_array( + array($helper_class, $function), + array_merge(array($this), $args)); + } + } + + if (Session::instance()->get("debug")) { + if ($function != "admin_head") { + array_unshift( + $blocks, "
    " . + "
    $function
    "); + $blocks[] = "
    "; + } + } + + return implode("\n", $blocks); + + default: + throw new Exception("@todo UNKNOWN_THEME_FUNCTION: $function"); + } + } +} \ No newline at end of file diff --git a/modules/gallery/libraries/Block.php b/modules/gallery/libraries/Block.php new file mode 100644 index 00000000..6fe679f1 --- /dev/null +++ b/modules/gallery/libraries/Block.php @@ -0,0 +1,30 @@ +__toString(); + } +} diff --git a/modules/gallery/libraries/I18n.php b/modules/gallery/libraries/I18n.php new file mode 100644 index 00000000..c936be88 --- /dev/null +++ b/modules/gallery/libraries/I18n.php @@ -0,0 +1,410 @@ +translate($message, $options); +} + +/** + * Translates a localizable message with plural forms. + * @param $singular String The message to be translated. E.g. "There is one album." + * @param $plural String The plural message to be translated. E.g. + * "There are %count albums." + * @param $count Number The number which is inserted for the %count placeholder and + * which is used to select the proper plural form ($singular or $plural). + * @param $options array (optional) Options array for key value pairs which are used + * for pluralization and interpolation. Special key: "locale" to override the + * currently configured locale. + * @return String The translated message string. + */ +function t2($singular, $plural, $count, $options=array()) { + return I18n::instance()->translate(array("one" => $singular, "other" => $plural), + array_merge($options, array("count" => $count))); +} + +class I18n_Core { + private static $_instance; + private $_config = array(); + private $_call_log = array(); + private $_cache = array(); + + private function __construct($config) { + $this->_config = $config; + $this->locale($config['default_locale']); + } + + public static function instance($config=null) { + if (self::$_instance == NULL || isset($config)) { + $config = isset($config) ? $config : Kohana::config('locale'); + if (empty($config['default_locale'])) { + $config['default_locale'] = module::get_var('core', 'default_locale'); + } + self::$_instance = new I18n_Core($config); + } + + return self::$_instance; + } + + public function locale($locale=null) { + if ($locale) { + $this->_config['default_locale'] = $locale; + // Attempt to set PHP's locale as well (for number formatting, collation, etc.) + // TODO: See G2 for better fallack code. + $locale_prefs = array($locale); + $locale_prefs[] = 'en_US'; + setlocale(LC_ALL, $locale_prefs); + } + return $this->_config['default_locale']; + } + + /** + * Translates a localizable message. + * @param $message String|array The message to be translated. E.g. "Hello world" + * or array("one" => "One album", "other" => "%count albums") + * @param $options array (optional) Options array for key value pairs which are used + * for pluralization and interpolation. Special keys are "count" and "locale", + * the latter to override the currently configured locale. + * @return String The translated message string. + */ + public function translate($message, $options=array()) { + $locale = empty($options['locale']) ? $this->_config['default_locale'] : $options['locale']; + $count = isset($options['count']) ? $options['count'] : null; + $values = $options; + unset($values['locale']); + $this->log($message, $options); + + $entry = $this->lookup($locale, $message); + + if (null === $entry) { + // Default to the root locale. + $entry = $message; + $locale = $this->_config['root_locale']; + } + + $entry = $this->pluralize($locale, $entry, $count); + + $entry = $this->interpolate($locale, $entry, $values); + + return $entry; + } + + private function lookup($locale, $message) { + if (!isset($this->_cache[$locale])) { + $this->_cache[$locale] = array(); + // TODO: Load data from locale file instead of the DB. + foreach (Database::instance() + ->select("key", "translation") + ->from("incoming_translations") + ->where(array("locale" => $locale)) + ->get() + ->as_array() as $row) { + $this->_cache[$locale][$row->key] = unserialize($row->translation); + } + + // Override incoming with outgoing... + foreach (Database::instance() + ->select("key", "translation") + ->from("outgoing_translations") + ->where(array("locale" => $locale)) + ->get() + ->as_array() as $row) { + $this->_cache[$locale][$row->key] = unserialize($row->translation); + } + } + + $key = self::get_message_key($message); + + if (isset($this->_cache[$locale][$key])) { + return $this->_cache[$locale][$key]; + } else { + return null; + } + } + + public function has_translation($message, $options=null) { + $locale = empty($options['locale']) ? $this->_config['default_locale'] : $options['locale']; + $count = empty($options['count']) ? null : $options['count']; + $values = $options; + unset($values['locale']); + $this->log($message, $options); + + $entry = $this->lookup($locale, $message); + + if (null === $entry) { + return false; + } else if (!is_array($entry)) { + return $entry !== ''; + } else { + $plural_key = self::get_plural_key($locale, $count); + return isset($entry[$plural_key]) + && $entry[$plural_key] !== null + && $entry[$plural_key] !== ''; + } + } + + public static function get_message_key($message) { + $as_string = is_array($message) ? implode('|', $message) : $message; + return md5($as_string); + } + + private function interpolate($locale, $string, $values) { + // TODO: Handle locale specific number formatting. + + // Replace x_y before replacing x. + krsort($values, SORT_STRING); + + $keys = array(); + foreach (array_keys($values) as $key) { + $keys[] = "%$key"; + } + return str_replace($keys, array_values($values), $string); + } + + private function pluralize($locale, $entry, $count) { + if (!is_array($entry)) { + return $entry; + } + + $plural_key = self::get_plural_key($locale, $count); + if (!isset($entry[$plural_key])) { + // Fallback to the default plural form. + $plural_key = 'other'; + } + + if (isset($entry[$plural_key])) { + return $entry[$plural_key]; + } else { + // Fallback to just any plural form. + list ($plural_key, $string) = each($entry); + return $string; + } + } + + private function log($message, $options) { + $key = self::get_message_key($message); + isset($this->_call_log[$key]) or $this->_call_log[$key] = array($message, $options); + } + + public function call_log() { + return $this->_call_log; + } + + private static function get_plural_key($locale, $count) { + $parts = explode('_', $locale); + $language = $parts[0]; + + // Data from CLDR 1.6 (http://unicode.org/cldr/data/common/supplemental/plurals.xml). + // Docs: http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html + switch ($language) { + case 'az': + case 'fa': + case 'hu': + case 'ja': + case 'ko': + case 'my': + case 'to': + case 'tr': + case 'vi': + case 'yo': + case 'zh': + case 'bo': + case 'dz': + case 'id': + case 'jv': + case 'ka': + case 'km': + case 'kn': + case 'ms': + case 'th': + return 'other'; + + case 'ar': + if ($count == 0) { + return 'zero'; + } else if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else if (is_int($count) && ($i = $count % 100) >= 3 && $i <= 10) { + return 'few'; + } else if (is_int($count) && ($i = $count % 100) >= 11 && $i <= 99) { + return 'many'; + } else { + return 'other'; + } + + case 'pt': + case 'am': + case 'bh': + case 'fil': + case 'tl': + case 'guw': + case 'hi': + case 'ln': + case 'mg': + case 'nso': + case 'ti': + case 'wa': + if ($count == 0 || $count == 1) { + return 'one'; + } else { + return 'other'; + } + + case 'fr': + if ($count >= 0 and $count < 2) { + return 'one'; + } else { + return 'other'; + } + + case 'lv': + if ($count == 0) { + return 'zero'; + } else if ($count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else { + return 'other'; + } + + case 'ga': + case 'se': + case 'sma': + case 'smi': + case 'smj': + case 'smn': + case 'sms': + if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else { + return 'other'; + } + + case 'ro': + case 'mo': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && $count == 0 && ($i = $count % 100) >= 1 && $i <= 19) { + return 'few'; + } else { + return 'other'; + } + + case 'lt': + if (is_int($count) && $count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 9 && ($i = $count % 100) < 11 && $i > 19) { + return 'few'; + } else { + return 'other'; + } + + case 'hr': + case 'ru': + case 'sr': + case 'uk': + case 'be': + case 'bs': + case 'sh': + if (is_int($count) && $count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 4 && ($i = $count % 100) < 12 && $i > 14) { + return 'few'; + } else if (is_int($count) && ($count % 10 == 0 || (($i = $count % 10) >= 5 && $i <= 9) || (($i = $count % 100) >= 11 && $i <= 14))) { + return 'many'; + } else { + return 'other'; + } + + case 'cs': + case 'sk': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && $count >= 2 && $count <= 4) { + return 'few'; + } else { + return 'other'; + } + + case 'pl': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 4 && + ($i = $count % 100) < 12 && $i > 14 && ($i = $count % 100) < 22 && $i > 24) { + return 'few'; + } else { + return 'other'; + } + + case 'sl': + if ($count % 100 == 1) { + return 'one'; + } else if ($count % 100 == 2) { + return 'two'; + } else if (is_int($count) && ($i = $count % 100) >= 3 && $i <= 4) { + return 'few'; + } else { + return 'other'; + } + + case 'mt': + if ($count == 1) { + return 'one'; + } else if ($count == 0 || is_int($count) && ($i = $count % 100) >= 2 && $i <= 10) { + return 'few'; + } else if (is_int($count) && ($i = $count % 100) >= 11 && $i <= 19) { + return 'many'; + } else { + return 'other'; + } + + case 'mk': + if ($count % 10 == 1) { + return 'one'; + } else { + return 'other'; + } + + case 'cy': + if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else if ($count == 8 || $count == 11) { + return 'many'; + } else { + return 'other'; + } + + default: // en, de, etc. + return $count == 1 ? 'one' : 'other'; + } + } +} \ No newline at end of file diff --git a/modules/gallery/libraries/MY_Database.php b/modules/gallery/libraries/MY_Database.php new file mode 100644 index 00000000..c56f16e8 --- /dev/null +++ b/modules/gallery/libraries/MY_Database.php @@ -0,0 +1,92 @@ +where[] = "("; + return $this; + } + + public function close_paren() { + // Search backwards for the last opening paren and resolve it + $i = count($this->where) - 1; + $this->where[$i] .= ")"; + while (--$i >= 0) { + if ($this->where[$i] == "(") { + // Remove the paren from the where clauses, and add it to the right of the operator of the + // next where clause. If removing the paren makes the next where clause the first element + // in the where list, then the operator shouldn't be there. It's there because we + // calculate whether or not we need an operator based on the number of where clauses, and + // the open paren seems like a where clause even though it isn't. + array_splice($this->where, $i, 1); + $this->where[$i] = preg_replace("/^(AND|OR) /", $i ? "\\1 (" : "(", $this->where[$i]); + return $this; + } + } + + throw new Kohana_Database_Exception('database.missing_open_paren'); + } + + /** + * Parse the query string and convert any strings of the form `\([a-zA-Z0-9_]*?)\] + * table prefix . $1 + */ + public function query($sql = '') { + if (!empty($sql)) { + $sql = $this->add_table_prefixes($sql); + } + return parent::query($sql); + } + + public function add_table_prefixes($sql) { + $prefix = $this->config["table_prefix"]; + if (strpos($sql, "SHOW TABLES") === 0) { + /* + * Don't ignore "show tables", otherwise we could have a infinite + * @todo this may have to be changed if we support more than mysql + */ + return $sql; + } else if (strpos($sql, "CREATE TABLE") === 0) { + // Creating a new table add it to the table cache. + $open_brace = strpos($sql, "{") + 1; + $close_brace = strpos($sql, "}", $open_brace); + $name = substr($sql, $open_brace, $close_brace - $open_brace); + $this->_table_names["{{$name}}"] = "{$prefix}$name"; + } + + if (!isset($this->_table_names)) { + // This should only run once on the first query + $this->_table_names =array(); + $len = strlen($prefix); + foreach($this->list_tables() as $table_name) { + if ($len > 0) { + $naked_name = strpos($table_name, $prefix) !== 0 ? + $table_name : substr($table_name, $len); + } else { + $naked_name = $table_name; + } + $this->_table_names["{{$naked_name}}"] = $table_name; + } + } + + return empty($this->_table_names) ? $sql : strtr($sql, $this->_table_names); + } +} \ No newline at end of file diff --git a/modules/gallery/libraries/MY_Forge.php b/modules/gallery/libraries/MY_Forge.php new file mode 100644 index 00000000..17d0465b --- /dev/null +++ b/modules/gallery/libraries/MY_Forge.php @@ -0,0 +1,59 @@ +hidden("csrf")->value(""); + } + /** + * Use our own template + */ + public function render($template="form.html", $custom=false) { + $this->hidden["csrf"]->value(access::csrf_token()); + return parent::render($template, $custom); + } + + /** + * Associate validation rules defined in the model with this form. + */ + public function add_rules_from($model) { + foreach ($this->inputs as $name => $input) { + if (isset($input->inputs)) { + $input->add_rules_from($model); + } + if (isset($model->rules[$name])) { + $input->rules($model->rules[$name]); + } + } + } + + /** + * Validate our CSRF value as a mandatory part of all form validation. + */ + public function validate() { + $status = parent::validate(); + access::verify_csrf(); + return $status; + } +} \ No newline at end of file diff --git a/modules/gallery/libraries/MY_ORM.php b/modules/gallery/libraries/MY_ORM.php new file mode 100644 index 00000000..fb2f80a7 --- /dev/null +++ b/modules/gallery/libraries/MY_ORM.php @@ -0,0 +1,46 @@ +db->open_paren(); + return $this; + } + + public function close_paren() { + $this->db->close_paren(); + return $this; + } +} + +/** + * Slide this in here for convenience. We won't ever be overloading ORM_Iterator without ORM. + */ +class ORM_Iterator extends ORM_Iterator_Core { + /** + * Cache the result row + */ + public function current() { + $row = parent::current(); + if (is_object($row)) { + model_cache::set($row); + } + return $row; + } +} \ No newline at end of file diff --git a/modules/gallery/libraries/MY_Pagination.php b/modules/gallery/libraries/MY_Pagination.php new file mode 100644 index 00000000..d06a974f --- /dev/null +++ b/modules/gallery/libraries/MY_Pagination.php @@ -0,0 +1,35 @@ +auto_hide === TRUE AND $this->total_pages <= 1) { + return ""; + } + + if ($style === NULL) { + // Use default style + $style = $this->style; + } + + // Return rendered pagination view + return View::factory("pager.html", get_object_vars($this))->render(); + } +} diff --git a/modules/gallery/libraries/MY_View.php b/modules/gallery/libraries/MY_View.php new file mode 100644 index 00000000..836d1087 --- /dev/null +++ b/modules/gallery/libraries/MY_View.php @@ -0,0 +1,46 @@ +set_global("csrf", access::csrf_token()); + } + + /** + * Override View_Core::render so that we trap errors stemming from bad PHP includes and show a + * visible stack trace to help developers. + * + * @see View_Core::render + */ + public function render($print=false, $renderer=false) { + try { + return parent::render($print, $renderer); + } catch (Exception $e) { + Kohana::Log('error', $e->getTraceAsString()); + Kohana::Log('debug', $e->getMessage()); + return ""; + } + } +} diff --git a/modules/gallery/libraries/Menu.php b/modules/gallery/libraries/Menu.php new file mode 100644 index 00000000..d19d8b1e --- /dev/null +++ b/modules/gallery/libraries/Menu.php @@ -0,0 +1,187 @@ +id = $id; + return $this; + } + + /** + * Set the label + * @chainable + */ + public function label($label) { + $this->label = $label; + return $this; + } + + /** + * Set the url + * @chainable + */ + public function url($url) { + $this->url = $url; + return $this; + } + + /** + * Set the css id + * @chainable + */ + public function css_id($css_id) { + $this->css_id = $css_id; + return $this; + } + + /** + * Set the css class + * @chainable + */ + public function css_class($css_class) { + $this->css_class = $css_class; + return $this; + } + +} + +/** + * Menu element that provides a link to a new page. + */ +class Menu_Element_Link extends Menu_Element { + public function __toString() { + if (isset($this->css_id) && !empty($this->css_id)) { + $css_id = " id=\"$this->css_id\""; + } else { + $css_id = ""; + } + if (isset($this->css_class) && !empty($this->css_class)) { + $css_class = " $this->css_class"; + } else { + $css_class = ""; + } + return "
  • url\" " . + "title=\"$this->label\">$this->label
  • "; + } +} + +/** + * Menu element that provides a pop-up dialog + */ +class Menu_Element_Dialog extends Menu_Element { + public function __toString() { + if (isset($this->css_id) && !empty($this->css_id)) { + $css_id = " id=\"$this->css_id\""; + } else { + $css_id = ""; + } + if (isset($this->css_class) && !empty($this->css_class)) { + $css_class = " $this->css_class"; + } else { + $css_class = ""; + } + return "
  • url\" " . + "title=\"$this->label\">$this->label
  • "; + } +} + +/** + * Root menu or submenu + */ +class Menu_Core extends Menu_Element { + public $elements; + public $is_root = false; + + /** + * Return an instance of a Menu_Element + * @chainable + */ + public static function factory($type) { + switch($type) { + case "link": + return new Menu_Element_Link(); + + case "dialog": + return new Menu_Element_Dialog(); + + case "root": + $menu = new Menu(); + $menu->is_root = true; + return $menu; + + case "submenu": + return new Menu(); + + default: + throw Exception("@todo UNKNOWN_MENU_TYPE"); + } + } + + public function __construct() { + $this->elements = array(); + } + + /** + * Add a new element to this menu + */ + public function append($menu_element) { + $this->elements[$menu_element->id] = $menu_element; + return $this; + } + + /** + * Add a new element to this menu + */ + public function add_after($target_id, $new_menu_element) { + $copy = array(); + foreach ($this->elements as $id => $menu_element) { + $copy[$id] = $menu_element; + if ($id == $target_id) { + $copy[$new_menu_element->id] = $new_menu_element; + } + } + $this->elements = $copy; + return $this; + } + + /** + * Retrieve a Menu_Element by id + */ + public function get($id) { + return $this->elements[$id]; + } + + public function __toString() { + $html = $this->is_root ? "
      " : + "
    • $this->label
        "; + $html .= implode("\n", $this->elements); + $html .= $this->is_root ? "
      " : "
    "; + return $html; + } +} diff --git a/modules/gallery/libraries/ORM_MPTT.php b/modules/gallery/libraries/ORM_MPTT.php new file mode 100644 index 00000000..46280d95 --- /dev/null +++ b/modules/gallery/libraries/ORM_MPTT.php @@ -0,0 +1,307 @@ +model_name = inflector::singular($this->table_name); + } + + /** + * Add this node as a child of the parent provided. + * + * @chainable + * @param integer $parent_id the id of the parent node + * @return ORM + */ + function add_to_parent($parent) { + $this->lock(); + + try { + // Make a hole in the parent for this new item + $this->db->query( + "UPDATE {{$this->table_name}} SET `left` = `left` + 2 WHERE `left` >= {$parent->right}"); + $this->db->query( + "UPDATE {{$this->table_name}} SET `right` = `right` + 2 WHERE `right` >= {$parent->right}"); + $parent->right += 2; + + // Insert this item into the hole + $this->left = $parent->right - 2; + $this->right = $parent->right - 1; + $this->parent_id = $parent->id; + $this->level = $parent->level + 1; + $this->save(); + $parent->reload(); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + + $this->unlock(); + return $this; + } + + /** + * Delete this node and all of its children. + */ + public function delete() { + $children = $this->children(); + if ($children) { + foreach ($this->children() as $item) { + // Deleting children affects the MPTT tree, so we have to reload each child before we + // delete it so that we have current left/right pointers. This is inefficient. + // @todo load each child once, not twice. + $item->reload()->delete(); + } + + // Deleting children has affected this item + $this->reload(); + } + + $this->lock(); + try { + $this->db->query( + "UPDATE {{$this->table_name}} SET `left` = `left` - 2 WHERE `left` > {$this->right}"); + $this->db->query( + "UPDATE {{$this->table_name}} SET `right` = `right` - 2 WHERE `right` > {$this->right}"); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + + $this->unlock(); + parent::delete(); + } + + /** + * Return true if the target is descendant of this item. + * @param ORM $target + * @return boolean + */ + function is_descendant($target) { + return ($this->left <= $target->left && $this->right >= $target->right); + } + + /** + * Return the parent of this node + * + * @return ORM + */ + function parent() { + if (!$this->parent_id) { + return null; + } + return model_cache::get($this->model_name, $this->parent_id); + } + + /** + * Return all the parents of this node, in order from root to this node's immediate parent. + * + * @return array ORM + */ + function parents() { + return $this + ->where("`left` <= {$this->left}") + ->where("`right` >= {$this->right}") + ->where("id <> {$this->id}") + ->orderby("left", "ASC") + ->find_all(); + } + + /** + * Return all of the children of this node, ordered by id. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @param array orderby + * @return array ORM + */ + function children($limit=null, $offset=0, $orderby=null) { + $this->where("parent_id", $this->id); + if (empty($orderby)) { + $this->orderby("id", "ASC"); + } else { + $this->orderby($orderby); + } + return $this->find_all($limit, $offset); + } + + /** + * Return all of the children of this node, ordered by id. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @return array ORM + */ + function children_count() { + return $this->where("parent_id", $this->id)->count_all(); + } + + /** + * Return all of the children of the specified type, ordered by id. + * + * @param integer SQL limit + * @param integer SQL offset + * @param string type to return + * @param array orderby + * @return object ORM_Iterator + */ + function descendants($limit=null, $offset=0, $type=null, $orderby=null) { + $this->where("left >", $this->left) + ->where("right <=", $this->right); + if ($type) { + $this->where("type", $type); + } + + if (empty($orderby)) { + $this->orderby("id", "ASC"); + } else { + $this->orderby($orderby); + } + + return $this->find_all($limit, $offset); + } + + /** + * Return the count of all the children of the specified type. + * + * @param string type to count + * @return integer child count + */ + function descendants_count($type=null) { + $this->where("left >", $this->left) + ->where("right <=", $this->right); + if ($type) { + $this->where("type", $type); + } + return $this->count_all(); + } + + /** + * Move this item to the specified target. + * + * @chainable + * @param Item_Model $target Target node + * @return ORM_MTPP + */ + function move_to($target) { + if ($this->left <= $target->left && + $this->right >= $target->right) { + throw new Exception("@todo INVALID_TARGET can't move item inside itself"); + } + + $number_to_move = (int)(($this->right - $this->left) / 2 + 1); + $size_of_hole = $number_to_move * 2; + $original_left = $this->left; + $original_right = $this->right; + $target_right = $target->right; + $level_delta = ($target->level + 1) - $this->level; + + $this->lock(); + try { + if ($level_delta) { + // Update the levels for the to-be-moved items + $this->db->query( + "UPDATE {{$this->table_name}} SET `level` = `level` + $level_delta" . + " WHERE `left` >= $original_left AND `right` <= $original_right"); + } + + // Make a hole in the target for the move + $target->db->query( + "UPDATE {{$this->table_name}} SET `left` = `left` + $size_of_hole" . + " WHERE `left` >= $target_right"); + $target->db->query( + "UPDATE {{$this->table_name}} SET `right` = `right` + $size_of_hole" . + " WHERE `right` >= $target_right"); + + // Change the parent. + $this->db->query( + "UPDATE {{$this->table_name}} SET `parent_id` = {$target->id}" . + " WHERE `id` = {$this->id}"); + + // If the source is to the right of the target then we just adjusted its left and right above. + $left = $original_left; + $right = $original_right; + if ($original_left > $target_right) { + $left += $size_of_hole; + $right += $size_of_hole; + } + + $new_offset = $target->right - $left; + $this->db->query( + "UPDATE {{$this->table_name}}" . + " SET `left` = `left` + $new_offset," . + " `right` = `right` + $new_offset" . + " WHERE `left` >= $left" . + " AND `right` <= $right"); + + // Close the hole in the source's parent after the move + $this->db->query( + "UPDATE {{$this->table_name}} SET `left` = `left` - $size_of_hole" . + " WHERE `left` > $right"); + $this->db->query( + "UPDATE {{$this->table_name}} SET `right` = `right` - $size_of_hole" . + " WHERE `right` > $right"); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + + $this->unlock(); + + // Lets reload to get the changes. + $this->reload(); + return $this; + } + + /** + * Lock the tree to prevent concurrent modification. + */ + protected function lock() { + $result = $this->db->query("SELECT GET_LOCK('{$this->table_name}', 1) AS l")->current(); + if (empty($result->l)) { + throw new Exception("@todo UNABLE_TO_LOCK_EXCEPTION"); + } + } + + /** + * Unlock the tree. + */ + protected function unlock() { + $this->db->query("SELECT RELEASE_LOCK('{$this->table_name}')"); + } +} diff --git a/modules/gallery/libraries/Sendmail.php b/modules/gallery/libraries/Sendmail.php new file mode 100644 index 00000000..90998457 --- /dev/null +++ b/modules/gallery/libraries/Sendmail.php @@ -0,0 +1,97 @@ +headers = array(); + $config = Kohana::config("sendmail"); + foreach ($config as $key => $value) { + $this->$key($value); + } + } + + public function __get($key) { + return null; + } + + public function __call($key, $value) { + switch ($key) { + case "to": + $this->to = is_array($value[0]) ? $value[0] : array($value[0]); + break; + case "header": + if (count($value) != 2) { + throw new Exception("@todo INVALID_HEADER_PARAMETERS"); + } + $this->headers[$value[0]] = $value[1]; + break; + case "from": + $this->headers["From"] = $value[0]; + break; + case "reply_to": + $this->headers["Reply-To"] = $value[0]; + break; + default: + $this->$key = $value[0]; + } + return $this; + } + + public function send() { + if (empty($this->to)) { + throw new Exception("@todo TO_IS_REQUIRED_FOR_MAIL"); + } + $to = implode(", ", $this->to); + $headers = array(); + foreach ($this->headers as $key => $value) { + $key = ucfirst($key); + $headers[] = "$key: $value"; + } + + // The docs say headers should be separated by \r\n, but occasionaly that doesn't work and you + // need to use a single \n. This can be set in config/sendmail.php + $headers = implode($this->header_separator, $headers); + $message = wordwrap($this->message, $this->line_length, "\n"); + if (!$this->mail($to, $this->subject, $message, $headers)) { + Kohana::log("error", wordwrap("Sending mail failed:\nTo: $to\n $this->subject\n" . + "Headers: $headers\n $this->message")); + throw new Exception("@todo SEND_MAIL_FAILED"); + } + return $this; + } + + public function mail($to, $subject, $message, $headers) { + return mail($to, $subject, $message, $headers); + } +} diff --git a/modules/gallery/libraries/Task_Definition.php b/modules/gallery/libraries/Task_Definition.php new file mode 100644 index 00000000..8d9c5922 --- /dev/null +++ b/modules/gallery/libraries/Task_Definition.php @@ -0,0 +1,50 @@ +callback = $callback; + return $this; + } + + function description($description) { + $this->description = $description; + return $this; + } + + function name($name) { + $this->name = $name; + return $this; + } + + function severity($severity) { + $this->severity = $severity; + return $this; + } +} diff --git a/modules/gallery/libraries/Theme_View.php b/modules/gallery/libraries/Theme_View.php new file mode 100644 index 00000000..b5b97666 --- /dev/null +++ b/modules/gallery/libraries/Theme_View.php @@ -0,0 +1,221 @@ +theme_name = module::get_var("core", "active_site_theme"); + if (user::active()->admin) { + $this->theme_name = Input::instance()->get("theme", $this->theme_name); + } + $this->item = null; + $this->tag = null; + $this->set_global("theme", $this); + $this->set_global("user", user::active()); + $this->set_global("page_type", $page_type); + if ($page_type == "album") { + $this->set_global("thumb_proportion", $this->thumb_proportion()); + } + + $maintenance_mode = Kohana::config("core.maintenance_mode", false, false); + if ($maintenance_mode) { + message::warning(t("This site is currently in maintenance mode")); + } + } + + /** + * Proportion of the current thumb_size's to default + * @return int + */ + public function thumb_proportion() { + // @TODO change the 200 to a theme supplied value when and if we come up with an + // API to allow the theme to set defaults. + return module::get_var("core", "thumb_size", 200) / 200; + } + + public function url($path, $absolute_url=false) { + $arg = "themes/{$this->theme_name}/$path"; + return $absolute_url ? url::abs_file($arg) : url::file($arg); + } + + public function item() { + return $this->item; + } + + public function tag() { + return $this->tag; + } + + public function page_type() { + return $this->page_type; + } + + public function display($page_name, $view_class="View") { + return new $view_class($page_name); + } + + public function site_menu() { + $menu = Menu::factory("root"); + if ($this->page_type != "login") { + core_menu::site($menu, $this); + + foreach (module::active() as $module) { + if ($module->name == "core") { + continue; + } + $class = "{$module->name}_menu"; + if (method_exists($class, "site")) { + call_user_func_array(array($class, "site"), array(&$menu, $this)); + } + } + } + + print $menu; + } + + public function album_menu() { + $menu = Menu::factory("root"); + core_menu::album($menu, $this); + + foreach (module::active() as $module) { + if ($module->name == "core") { + continue; + } + $class = "{$module->name}_menu"; + if (method_exists($class, "album")) { + call_user_func_array(array($class, "album"), array(&$menu, $this)); + } + } + + print $menu; + } + + public function photo_menu() { + $menu = Menu::factory("root"); + core_menu::photo($menu, $this); + + foreach (module::active() as $module) { + if ($module->name == "core") { + continue; + } + $class = "{$module->name}_menu"; + if (method_exists($class, "photo")) { + call_user_func_array(array($class, "photo"), array(&$menu, $this)); + } + } + + print $menu; + } + + public function pager() { + if ($this->children_count) { + $this->pagination = new Pagination(); + $this->pagination->initialize( + array('query_string' => 'page', + 'total_items' => $this->children_count, + 'items_per_page' => $this->page_size, + 'style' => 'classic')); + return $this->pagination->render(); + } + } + + /** + * Print out any site wide status information. + */ + public function site_status() { + return site_status::get(); + } + + /** + * Print out any messages waiting for this user. + */ + public function messages() { + return message::get(); + } + + /** + * Handle all theme functions that insert module content. + */ + public function __call($function, $args) { + switch ($function) { + case "album_blocks": + case "album_bottom": + case "album_top": + case "credits"; + case "dynamic_bottom": + case "dynamic_top": + case "footer": + case "head": + case "header_bottom": + case "header_top": + case "page_bottom": + case "page_top": + case "photo_blocks": + case "photo_bottom": + case "photo_top": + case "resize_bottom": + case "resize_top": + case "sidebar_blocks": + case "sidebar_bottom": + case "sidebar_top": + case "thumb_bottom": + case "thumb_info": + case "thumb_top": + $blocks = array(); + foreach (module::active() as $module) { + $helper_class = "{$module->name}_theme"; + if (method_exists($helper_class, $function)) { + $blocks[] = call_user_func_array( + array($helper_class, $function), + array_merge(array($this), $args)); + } + } + if (Session::instance()->get("debug")) { + if ($function != "head") { + array_unshift( + $blocks, "
    " . + "
    $function
    "); + $blocks[] = "
    "; + } + } + return implode("\n", $blocks); + + default: + throw new Exception("@todo UNKNOWN_THEME_FUNCTION: $function"); + } + } +} \ No newline at end of file diff --git a/modules/gallery/models/access_cache.php b/modules/gallery/models/access_cache.php new file mode 100644 index 00000000..10d05df7 --- /dev/null +++ b/modules/gallery/models/access_cache.php @@ -0,0 +1,21 @@ + "required|length[0,255]", + "title" => "required|length[0,255]", + "description" => "length[0,65535]" + ); + + /** + * Add a set of restrictions to any following queries to restrict access only to items + * viewable by the active user. + * @chainable + */ + public function viewable() { + if (is_null($this->view_restrictions)) { + if (user::active()->admin) { + $this->view_restrictions = array(); + } else { + foreach (user::group_ids() as $id) { + // Separate the first restriction from the rest to make it easier for us to formulate + // our where clause below + if (empty($this->view_restrictions)) { + $this->view_restrictions[0] = "view_$id"; + } else { + $this->view_restrictions[1]["view_$id"] = access::ALLOW; + } + } + } + } + switch (count($this->view_restrictions)) { + case 0: + break; + + case 1: + $this->where($this->view_restrictions[0], access::ALLOW); + break; + + default: + $this->open_paren(); + $this->where($this->view_restrictions[0], access::ALLOW); + $this->orwhere($this->view_restrictions[1]); + $this->close_paren(); + break; + } + + return $this; + } + + /** + * Is this item an album? + * @return true if it's an album + */ + public function is_album() { + return $this->type == 'album'; + } + + /** + * Is this item a photo? + * @return true if it's a photo + */ + public function is_photo() { + return $this->type == 'photo'; + } + + /** + * Is this item a movie? + * @return true if it's a movie + */ + public function is_movie() { + return $this->type == 'movie'; + } + + public function delete() { + module::event("item_before_delete", $this); + + $parent = $this->parent(); + if ($parent->album_cover_item_id == $this->id) { + item::remove_album_cover($parent); + } + + $path = $this->file_path(); + $resize_path = $this->resize_path(); + $thumb_path = $this->thumb_path(); + + parent::delete(); + if (is_dir($path)) { + @dir::unlink($path); + @dir::unlink(dirname($resize_path)); + @dir::unlink(dirname($thumb_path)); + } else { + @unlink($path); + @unlink($resize_path); + @unlink($thumb_path); + } + } + + /** + * Move this item to the specified target. + * @chainable + * @param Item_Model $target Target item (must be an album + * @return ORM_MTPP + */ + function move_to($target) { + if (!$target->is_album()) { + throw new Exception("@todo INVALID_MOVE_TYPE $target->type"); + } + + if ($this->id == 1) { + throw new Exception("@todo INVALID_SOURCE root album"); + } + + $original_path = $this->file_path(); + $original_resize_path = $this->resize_path(); + $original_thumb_path = $this->thumb_path(); + + parent::move_to($target, true); + $this->relative_path_cache = null; + + rename($original_path, $this->file_path()); + if ($this->is_album()) { + @rename(dirname($original_resize_path), dirname($this->resize_path())); + @rename(dirname($original_thumb_path), dirname($this->thumb_path())); + Database::instance() + ->update("items", + array("relative_path_cache" => null), + array("left >" => $this->left, "right <" => $this->right)); + } else { + @rename($original_resize_path, $this->resize_path()); + @rename($original_thumb_path, $this->thumb_path()); + } + + return $this; + } + + /** + * Rename the underlying file for this item to a new name. Move all the files. This requires a + * save. + * + * @chainable + */ + public function rename($new_name) { + if ($new_name == $this->name) { + return; + } + + if (strpos($new_name, "/")) { + throw new Exception("@todo NAME_CANNOT_CONTAIN_SLASH"); + } + + $old_relative_path = $this->relative_path(); + $new_relative_path = dirname($old_relative_path) . "/" . $new_name; + @rename(VARPATH . "albums/$old_relative_path", VARPATH . "albums/$new_relative_path"); + @rename(VARPATH . "resizes/$old_relative_path", VARPATH . "resizes/$new_relative_path"); + @rename(VARPATH . "thumbs/$old_relative_path", VARPATH . "thumbs/$new_relative_path"); + $this->name = $new_name; + + if ($this->is_album()) { + Database::instance() + ->update("items", + array("relative_path_cache" => null), + array("left >" => $this->left, "right <" => $this->right)); + } + + return $this; + } + + /** + * album: url::site("albums/2") + * photo: url::site("photos/3") + * + * @param string $query the query string (eg "show=3") + */ + public function url($query=array(), $full_uri=false) { + $url = ($full_uri ? url::abs_site("{$this->type}s/$this->id") + : url::site("{$this->type}s/$this->id")); + if ($query) { + $url .= "?$query"; + } + return $url; + } + + /** + * album: /var/albums/album1/album2 + * photo: /var/albums/album1/album2/photo.jpg + */ + public function file_path() { + return VARPATH . "albums/" . $this->relative_path(); + } + + /** + * album: http://example.com/gallery3/var/resizes/album1/ + * photo: http://example.com/gallery3/var/albums/album1/photo.jpg + */ + public function file_url($full_uri=false) { + return $full_uri ? + url::abs_file("var/albums/" . $this->relative_path()) : + url::file("var/albums/" . $this->relative_path()); + } + + /** + * album: /var/resizes/album1/.thumb.jpg + * photo: /var/albums/album1/photo.thumb.jpg + */ + public function thumb_path() { + $base = VARPATH . "thumbs/" . $this->relative_path(); + if ($this->is_photo()) { + return $base; + } else if ($this->is_album()) { + return $base . "/.album.jpg"; + } else if ($this->is_movie()) { + // Replace the extension with jpg + return preg_replace("/...$/", "jpg", $base); + } + } + + /** + * Return true if there is a thumbnail for this item. + */ + public function has_thumb() { + return $this->thumb_width && $this->thumb_height; + } + + /** + * album: http://example.com/gallery3/var/resizes/album1/.thumb.jpg + * photo: http://example.com/gallery3/var/albums/album1/photo.thumb.jpg + */ + public function thumb_url($full_uri=false) { + $cache_buster = "?m=" . $this->updated; + $base = ($full_uri ? + url::abs_file("var/thumbs/" . $this->relative_path()) : + url::file("var/thumbs/" . $this->relative_path())); + if ($this->is_photo()) { + return $base . $cache_buster; + } else if ($this->is_album()) { + return $base . "/.album.jpg" . $cache_buster; + } else if ($this->is_movie()) { + // Replace the extension with jpg + $base = preg_replace("/...$/", "jpg", $base); + return $base . $cache_buster; + } + } + + /** + * album: /var/resizes/album1/.resize.jpg + * photo: /var/albums/album1/photo.resize.jpg + */ + public function resize_path() { + return VARPATH . "resizes/" . $this->relative_path() . + ($this->is_album() ? "/.album.jpg" : ""); + } + + /** + * album: http://example.com/gallery3/var/resizes/album1/.resize.jpg + * photo: http://example.com/gallery3/var/albums/album1/photo.resize.jpg + */ + public function resize_url($full_uri=false) { + return ($full_uri ? + url::abs_file("var/resizes/" . $this->relative_path()) : + url::file("var/resizes/" . $this->relative_path())) . + ($this->is_album() ? "/.album.jpg" : ""); + } + + /** + * Return the relative path to this item's file. + * @return string + */ + public function relative_path() { + if (!isset($this->relative_path_cache)) { + $paths = array(); + foreach (Database::instance() + ->select("name") + ->from("items") + ->where("left <=", $this->left) + ->where("right >=", $this->right) + ->where("id <>", 1) + ->orderby("left", "ASC") + ->get() as $row) { + $paths[] = $row->name; + } + $this->relative_path_cache = implode($paths, "/"); + $this->save(); + } + return $this->relative_path_cache; + } + + /** + * @see ORM::__get() + */ + public function __get($column) { + if ($column == "owner") { + // This relationship depends on an outside module, which may not be present so handle + // failures gracefully. + try { + return model_cache::get("user", $this->owner_id); + } catch (Exception $e) { + return null; + } + } else { + return parent::__get($column); + } + } + + /** + * @see ORM::__set() + */ + public function __set($column, $value) { + if ($column == "name") { + // Clear the relative path as it is no longer valid. + $this->relative_path_cache = null; + } + parent::__set($column, $value); + } + + /** + * @see ORM::save() + */ + public function save() { + if (!empty($this->changed) && $this->changed != array("view_count" => "view_count")) { + $this->updated = time(); + if (!$this->loaded) { + $this->created = $this->updated; + $r = ORM::factory("item")->select("MAX(weight) as max_weight")->find(); + $this->weight = $r->max_weight + 1; + } + } + return parent::save(); + } + + /** + * Return the Item_Model representing the cover for this album. + * @return Item_Model or null if there's no cover + */ + public function album_cover() { + if (!$this->is_album()) { + return null; + } + + if (empty($this->album_cover_item_id)) { + return null; + } + + try { + return model_cache::get("item", $this->album_cover_item_id); + } catch (Exception $e) { + // It's possible (unlikely) that the item was deleted, if so keep going. + return null; + } + } + + /** + * Find the position of the given child id in this album. The resulting value is 1-indexed, so + * the first child in the album is at position 1. + */ + public function get_position($child_id) { + $result = Database::instance()->query(" + SELECT COUNT(*) AS position FROM {items} + WHERE parent_id = {$this->parent_id} + AND {$this->sort_column} <= (SELECT {$this->sort_column} + FROM {items} WHERE id = $child_id) + ORDER BY {$this->sort_column} {$this->sort_order}"); + + return $result->current()->position; + } + + /** + * Return an tag for the thumbnail. + * @param array $extra_attrs Extra attributes to add to the img tag + * @param int (optional) $max Maximum size of the thumbnail (default: null) + * @param boolean (optional) $center_vertically Center vertically (default: false) + * @return string + */ + public function thumb_tag($extra_attrs=array(), $max=null, $center_vertically=false) { + list ($height, $width) = $this->scale_dimensions($max); + if ($center_vertically && $max) { + // The constant is divide by 2 to calculate the file and 10 to convert to em + $margin_top = ($max - $height) / 20; + $extra_attrs["style"] = "margin-top: {$margin_top}em"; + $extra_attrs["title"] = $this->title; + } + $attrs = array_merge($extra_attrs, + array( + "src" => $this->thumb_url(), + "alt" => $this->title, + "width" => $width, + "height" => $height) + ); + // html::image forces an absolute url which we don't want + return ""; + } + + /** + * Calculate the largest width/height that fits inside the given maximum, while preserving the + * aspect ratio. + * @param int $max Maximum size of the largest dimension + * @return array + */ + public function scale_dimensions($max) { + $width = $this->thumb_width; + $height = $this->thumb_height; + + if ($height) { + if (isset($max)) { + if ($width > $height) { + $height = (int)($max * ($height / $width)); + $width = $max; + } else { + $width = (int)($max * ($width / $height)); + $height = $max; + } + } + } else { + // Missing thumbnail, can happen on albums with no photos yet. + // @todo we should enforce a placeholder for those albums. + $width = 0; + $height = 0; + } + return array($height, $width); + } + + /** + * Return an tag for the resize. + * @param array $extra_attrs Extra attributes to add to the img tag + * @return string + */ + public function resize_tag($extra_attrs) { + $attrs = array_merge($extra_attrs, + array("src" => $this->resize_url(), + "alt" => $this->title, + "width" => $this->resize_width, + "height" => $this->resize_height) + ); + // html::image forces an absolute url which we don't want + return ""; + } + + /** + * Return a flowplayer "; + } + + /** + * Return all of the children of this node, ordered by the defined sort order. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @return array ORM + */ + function children($limit=null, $offset=0) { + return parent::children($limit, $offset, array($this->sort_column => $this->sort_order)); + } + + /** + * Return all of the children of the specified type, ordered by the defined sort order. + * @param integer SQL limit + * @param integer SQL offset + * @param string type to return + * @return object ORM_Iterator + */ + function descendants($limit=null, $offset=0, $type=null) { + return parent::descendants($limit, $offset, $type, + array($this->sort_column => $this->sort_order)); + } +} diff --git a/modules/gallery/models/log.php b/modules/gallery/models/log.php new file mode 100644 index 00000000..6734afb8 --- /dev/null +++ b/modules/gallery/models/log.php @@ -0,0 +1,22 @@ +context); + if (array_key_exists($key, $context)) { + return $context[$key]; + } else { + return $default; + } + } + + public function set($key, $value) { + $context = unserialize($this->context); + $context[$key] = $value; + $this->context = serialize($context); + } + + public function save() { + if (!empty($this->changed)) { + $this->updated = time(); + } + return parent::save(); + } + + public function owner() { + return user::lookup($this->owner_id); + } +} \ No newline at end of file diff --git a/modules/gallery/models/theme.php b/modules/gallery/models/theme.php new file mode 100644 index 00000000..f479fd5a --- /dev/null +++ b/modules/gallery/models/theme.php @@ -0,0 +1,21 @@ +where("name", "access_test")->find(); + if ($group->loaded) { + $group->delete(); + } + } catch (Exception $e) { } + + try { + access::delete_permission("access_test"); + } catch (Exception $e) { } + + try { + $user = ORM::factory("user")->where("name", "access_test")->find(); + if ($user->loaded) { + $user->delete(); + } + } catch (Exception $e) { } + } + + public function setup() { + user::set_active(user::guest()); + } + + public function groups_and_permissions_are_bound_to_columns_test() { + access::register_permission("access_test", "Access Test"); + $group = group::create("access_test"); + + // We have a new column for this perm / group combo + $fields = Database::instance()->list_fields("access_caches"); + $this->assert_true(array_key_exists("access_test_{$group->id}", $fields)); + + access::delete_permission("access_test"); + $group->delete(); + + // Now the column has gone away + $fields = Database::instance()->list_fields("access_caches"); + $this->assert_false(array_key_exists("access_test_{$group->id}", $fields)); + } + + public function adding_and_removing_items_adds_ands_removes_rows_test() { + $root = ORM::factory("item", 1); + $item = album::create($root, rand(), "test album"); + + // New rows exist + $this->assert_true(ORM::factory("access_cache")->where("item_id", $item->id)->find()->loaded); + $this->assert_true(ORM::factory("access_intent")->where("item_id", $item->id)->find()->loaded); + + // Delete the item + $item->delete(); + + // Rows are gone + $this->assert_false(ORM::factory("access_cache")->where("item_id", $item->id)->find()->loaded); + $this->assert_false(ORM::factory("access_intent")->where("item_id", $item->id)->find()->loaded); + } + + public function new_photos_inherit_parent_permissions_test() { + $root = ORM::factory("item", 1); + + $album = album::create($root, rand(), "test album"); + access::allow(group::everybody(), "view", $album); + + $photo = ORM::factory("item"); + $photo->type = "photo"; + $photo->add_to_parent($album); + access::add_item($photo); + + $this->assert_true($photo->__get("view_" . group::everybody()->id)); + } + + public function can_allow_deny_and_reset_intent_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + $intent = ORM::factory("access_intent")->where("item_id", $album)->find(); + + // Allow + access::allow(group::everybody(), "view", $album); + $this->assert_same(access::ALLOW, $intent->reload()->view_1); + + // Deny + access::deny(group::everybody(), "view", $album); + $this->assert_same( + access::DENY, + ORM::factory("access_intent")->where("item_id", $album)->find()->view_1); + + // Allow again. If the initial value was allow, then the first Allow clause above may not + // have actually changed any values. + access::allow(group::everybody(), "view", $album); + $this->assert_same( + access::ALLOW, + ORM::factory("access_intent")->where("item_id", $album)->find()->view_1); + + access::reset(group::everybody(), "view", $album); + $this->assert_same( + null, + ORM::factory("access_intent")->where("item_id", $album)->find()->view_1); + } + + public function cant_reset_root_item_test() { + try { + access::reset(group::everybody(), "view", ORM::factory("item", 1)); + } catch (Exception $e) { + return; + } + $this->assert_true(false, "Should not be able to reset root intent"); + } + + public function can_view_item_test() { + $root = ORM::factory("item", 1); + access::allow(group::everybody(), "view", $root); + $this->assert_true(access::group_can(group::everybody(), "view", $root)); + } + + public function can_always_fails_on_unloaded_items_test() { + $root = ORM::factory("item", 1); + access::allow(group::everybody(), "view", $root); + $this->assert_true(access::group_can(group::everybody(), "view", $root)); + + $bogus = ORM::factory("item", -1); + $this->assert_false(access::group_can(group::everybody(), "view", $bogus)); + } + + public function cant_view_child_of_hidden_parent_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + + $root->reload(); + access::deny(group::everybody(), "view", $root); + access::reset(group::everybody(), "view", $album); + + $album->reload(); + $this->assert_false(access::group_can(group::everybody(), "view", $album)); + } + + public function view_permissions_propagate_down_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + + access::allow(group::everybody(), "view", $root); + access::reset(group::everybody(), "view", $album); + $album->reload(); + $this->assert_true(access::group_can(group::everybody(), "view", $album)); + } + + public function can_toggle_view_permissions_propagate_down_test() { + $root = ORM::factory("item", 1); + $album1 = album::create($root, rand(), "test album"); + $album2 = album::create($album1, rand(), "test album"); + $album3 = album::create($album2, rand(), "test album"); + $album4 = album::create($album3, rand(), "test album"); + + $album1->reload(); + $album2->reload(); + $album3->reload(); + $album4->reload(); + + access::allow(group::everybody(), "view", $root); + access::deny(group::everybody(), "view", $album1); + access::reset(group::everybody(), "view", $album2); + access::reset(group::everybody(), "view", $album3); + access::reset(group::everybody(), "view", $album4); + + $album4->reload(); + $this->assert_false(access::group_can(group::everybody(), "view", $album4)); + + access::allow(group::everybody(), "view", $album1); + $album4->reload(); + $this->assert_true(access::group_can(group::everybody(), "view", $album4)); + } + + public function revoked_view_permissions_cant_be_allowed_lower_down_test() { + $root = ORM::factory("item", 1); + $album1 = album::create($root, rand(), "test album"); + $album2 = album::create($album1, rand(), "test album"); + + $root->reload(); + access::deny(group::everybody(), "view", $root); + access::allow(group::everybody(), "view", $album2); + + $album1->reload(); + $this->assert_false(access::group_can(group::everybody(), "view", $album1)); + + $album2->reload(); + $this->assert_false(access::group_can(group::everybody(), "view", $album2)); + } + + public function can_edit_item_test() { + $root = ORM::factory("item", 1); + access::allow(group::everybody(), "edit", $root); + $this->assert_true(access::group_can(group::everybody(), "edit", $root)); + } + + public function non_view_permissions_propagate_down_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + + access::allow(group::everybody(), "edit", $root); + access::reset(group::everybody(), "edit", $album); + $this->assert_true(access::group_can(group::everybody(), "edit", $album)); + } + + public function non_view_permissions_can_be_revoked_lower_down_test() { + $root = ORM::factory("item", 1); + $outer = album::create($root, rand(), "test album"); + $outer_photo = ORM::factory("item"); + $outer_photo->type = "photo"; + $outer_photo->add_to_parent($outer); + access::add_item($outer_photo); + + $inner = album::create($outer, rand(), "test album"); + $inner_photo = ORM::factory("item"); + $inner_photo->type = "photo"; + $inner_photo->add_to_parent($inner); + access::add_item($inner_photo); + + $outer->reload(); + $inner->reload(); + + access::allow(group::everybody(), "edit", $root); + access::deny(group::everybody(), "edit", $outer); + access::allow(group::everybody(), "edit", $inner); + + // Outer album is not editable, inner one is. + $this->assert_false(access::group_can(group::everybody(), "edit", $outer_photo)); + $this->assert_true(access::group_can(group::everybody(), "edit", $inner_photo)); + } + + public function i_can_edit_test() { + // Create a new user that belongs to no groups + $user = user::create("access_test", "Access Test", ""); + foreach ($user->groups as $group) { + $user->remove($group); + } + $user->save(); + user::set_active($user); + + // This user can't edit anything + $root = ORM::factory("item", 1); + $this->assert_false(access::can("edit", $root)); + + // Now add them to a group that has edit permission + $group = group::create("access_test"); + $group->add($user); + $group->save(); + access::allow($group, "edit", $root); + + $user = ORM::factory("user", $user->id); // reload() does not flush related columns + user::set_active($user); + + // And verify that the user can edit. + $this->assert_true(access::can("edit", $root)); + } + + public function everybody_view_permission_maintains_htaccess_files_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + + access::deny(group::everybody(), "view", $album); + $this->assert_true(file_exists($album->file_path() . "/.htaccess")); + + access::allow(group::everybody(), "view", $album); + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + + access::deny(group::everybody(), "view", $album); + $this->assert_true(file_exists($album->file_path() . "/.htaccess")); + + access::reset(group::everybody(), "view", $album); + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + } + + public function everybody_view_full_permission_maintains_htaccess_files_test() { + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), "test album"); + + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + $this->assert_false(file_exists($album->resize_path() . "/.htaccess")); + $this->assert_false(file_exists($album->thumb_path() . "/.htaccess")); + + access::deny(group::everybody(), "view_full", $album); + $this->assert_true(file_exists($album->file_path() . "/.htaccess")); + $this->assert_false(file_exists($album->resize_path() . "/.htaccess")); + $this->assert_false(file_exists($album->thumb_path() . "/.htaccess")); + + access::allow(group::everybody(), "view_full", $album); + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + $this->assert_false(file_exists($album->resize_path() . "/.htaccess")); + $this->assert_false(file_exists($album->thumb_path() . "/.htaccess")); + + access::deny(group::everybody(), "view_full", $album); + $this->assert_true(file_exists($album->file_path() . "/.htaccess")); + $this->assert_false(file_exists($album->resize_path() . "/.htaccess")); + $this->assert_false(file_exists($album->thumb_path() . "/.htaccess")); + + access::reset(group::everybody(), "view_full", $album); + $this->assert_false(file_exists($album->file_path() . "/.htaccess")); + $this->assert_false(file_exists($album->resize_path() . "/.htaccess")); + $this->assert_false(file_exists($album->thumb_path() . "/.htaccess")); + } +} diff --git a/modules/gallery/tests/Album_Helper_Test.php b/modules/gallery/tests/Album_Helper_Test.php new file mode 100644 index 00000000..80afa8d1 --- /dev/null +++ b/modules/gallery/tests/Album_Helper_Test.php @@ -0,0 +1,87 @@ +assert_equal(VARPATH . "albums/$rand", $album->file_path()); + $this->assert_equal(VARPATH . "thumbs/$rand/.album.jpg", $album->thumb_path()); + $this->assert_true(is_dir(VARPATH . "thumbs/$rand"), "missing thumb dir"); + + // It's unclear that a resize makes sense for an album. But we have one. + $this->assert_equal(VARPATH . "resizes/$rand/.album.jpg", $album->resize_path()); + $this->assert_true(is_dir(VARPATH . "resizes/$rand"), "missing resizes dir"); + + $this->assert_equal(1, $album->parent_id); // MPTT tests will cover other hierarchy checks + $this->assert_equal($rand, $album->name); + $this->assert_equal($rand, $album->title); + $this->assert_equal($rand, $album->description); + } + + public function create_conflicting_album_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $album1 = album::create($root, $rand, $rand, $rand); + $album2 = album::create($root, $rand, $rand, $rand); + $this->assert_true($album1->name != $album2->name); + } + + public function thumb_url_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $album = album::create($root, $rand, $rand, $rand); + $this->assert_equal("http://./var/thumbs/$rand/.album.jpg", $album->thumb_url()); + } + + public function resize_url_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $album = album::create($root, $rand, $rand, $rand); + $this->assert_equal("http://./var/resizes/$rand/.album.jpg", $album->resize_url()); + } + + public function create_album_shouldnt_allow_names_with_slash_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + try { + $album = album::create($root, $rand . "/", $rand, $rand); + } catch (Exception $e) { + // pass + return; + } + + $this->assert_true(false, "Shouldn't create an album with / in the name"); + } + + public function create_album_silently_trims_trailing_periods_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + try { + $album = album::create($root, $rand . "..", $rand, $rand); + } catch (Exception $e) { + $this->assert_equal("@todo NAME_CANNOT_END_IN_PERIOD", $e->getMessage()); + return; + } + + $this->assert_true(false, "Shouldn't create an album with trailing . in the name"); + } +} diff --git a/modules/gallery/tests/Albums_Controller_Test.php b/modules/gallery/tests/Albums_Controller_Test.php new file mode 100644 index 00000000..ef1fac77 --- /dev/null +++ b/modules/gallery/tests/Albums_Controller_Test.php @@ -0,0 +1,76 @@ +_post = $_POST; + } + + public function teardown() { + $_POST = $this->_post; + } + + public function change_album_test() { + $controller = new Albums_Controller(); + $root = ORM::factory("item", 1); + $album = album::create($root, "test", "test", "test"); + $orig_name = $album->name; + + $_POST["dirname"] = "test"; + $_POST["name"] = "new name"; + $_POST["title"] = "new title"; + $_POST["description"] = "new description"; + $_POST["column"] = "weight"; + $_POST["direction"] = "ASC"; + $_POST["csrf"] = access::csrf_token(); + $_POST["_method"] = "put"; + access::allow(group::everybody(), "edit", $root); + + ob_start(); + $controller->_update($album); + $results = ob_get_contents(); + ob_end_clean(); + + $this->assert_equal( + json_encode(array("result" => "success", "location" => "http://./index.php/test")), + $results); + $this->assert_equal("new title", $album->title); + $this->assert_equal("new description", $album->description); + + // We don't change the name, yet. + $this->assert_equal($orig_name, $album->name); + } + + public function change_album_no_csrf_fails_test() { + $controller = new Albums_Controller(); + $root = ORM::factory("item", 1); + $album = album::create($root, "test", "test", "test"); + $_POST["name"] = "new name"; + $_POST["title"] = "new title"; + $_POST["description"] = "new description"; + access::allow(group::everybody(), "edit", $root); + + try { + $controller->_update($album); + $this->assert_true(false, "This should fail"); + } catch (Exception $e) { + // pass + } + } +} diff --git a/modules/gallery/tests/Core_Installer_Test.php b/modules/gallery/tests/Core_Installer_Test.php new file mode 100644 index 00000000..f7036286 --- /dev/null +++ b/modules/gallery/tests/Core_Installer_Test.php @@ -0,0 +1,50 @@ +assert_true(file_exists(VARPATH . "albums")); + $this->assert_true(file_exists(VARPATH . "resizes")); + } + + public function install_registers_core_module_test() { + $core = ORM::factory("module")->where("name", "core")->find(); + $this->assert_equal("core", $core->name); + + // This is probably too volatile to keep for long + $this->assert_equal(1, $core->version); + } + + public function install_creates_root_item_test() { + $max_right = ORM::factory("item") + ->select("MAX(`right`) AS `right`") + ->find()->right; + $root = ORM::factory('item')->find(1); + $this->assert_equal("Gallery", $root->title); + $this->assert_equal(1, $root->left); + $this->assert_equal($max_right, $root->right); + $this->assert_equal(null, $root->parent_id); + $this->assert_equal(1, $root->level); + } +} diff --git a/modules/gallery/tests/Database_Test.php b/modules/gallery/tests/Database_Test.php new file mode 100644 index 00000000..bd3d2f53 --- /dev/null +++ b/modules/gallery/tests/Database_Test.php @@ -0,0 +1,134 @@ +where("a", 1) + ->where("b", 2) + ->compile(); + $sql = str_replace("\n", " ", $sql); + $this->assert_same("SELECT * WHERE `a` = 1 AND `b` = 2", $sql); + } + + function compound_where_test() { + $sql = Database::instance() + ->where("outer1", 1) + ->open_paren() + ->where("inner1", 1) + ->orwhere("inner2", 2) + ->close_paren() + ->where("outer2", 2) + ->compile(); + $sql = str_replace("\n", " ", $sql); + $this->assert_same( + "SELECT * WHERE `outer1` = 1 AND (`inner1` = 1 OR `inner2` = 2) AND `outer2` = 2", + $sql); + } + + function group_first_test() { + $sql = Database::instance() + ->open_paren() + ->where("inner1", 1) + ->orwhere("inner2", 2) + ->close_paren() + ->where("outer1", 1) + ->where("outer2", 2) + ->compile(); + $sql = str_replace("\n", " ", $sql); + $this->assert_same( + "SELECT * WHERE (`inner1` = 1 OR `inner2` = 2) AND `outer1` = 1 AND `outer2` = 2", + $sql); + } + + function where_array_test() { + $sql = Database::instance() + ->where("outer1", 1) + ->open_paren() + ->where("inner1", 1) + ->orwhere(array("inner2" => 2, "inner3" => 3)) + ->close_paren() + ->compile(); + $sql = str_replace("\n", " ", $sql); + $this->assert_same( + "SELECT * WHERE `outer1` = 1 AND (`inner1` = 1 OR `inner2` = 2 OR `inner3` = 3)", + $sql); + } + + function notlike_test() { + $sql = Database::instance() + ->where("outer1", 1) + ->open_paren() + ->ornotlike("inner1", 1) + ->close_paren() + ->compile(); + $sql = str_replace("\n", " ", $sql); + $this->assert_same( + "SELECT * WHERE `outer1` = 1 OR ( `inner1` NOT LIKE '%1%')", + $sql); + } + + function prefix_replacement_test() { + $db = Database_For_Test::instance(); + $converted = $db->add_table_prefixes("CREATE TABLE IF NOT EXISTS {test_tables} ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(32) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + ENGINE=InnoDB DEFAULT CHARSET=utf8"); + $expected = "CREATE TABLE IF NOT EXISTS g3test_test_tables ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(32) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + ENGINE=InnoDB DEFAULT CHARSET=utf8"; + $this->assert_same($expected, $converted); + + $sql = "UPDATE {test_tables} SET `name` = '{test string}' " . + "WHERE `item_id` IN " . + " (SELECT `id` FROM {items} " . + " WHERE `left` >= 1 " . + " AND `right` <= 6)"; + $sql = $db->add_table_prefixes($sql); + + $expected = "UPDATE g3test_test_tables SET `name` = '{test string}' " . + "WHERE `item_id` IN " . + " (SELECT `id` FROM g3test_items " . + " WHERE `left` >= 1 " . + " AND `right` <= 6)"; + + $this->assert_same($expected, $sql); + } + + public function setup() { + } + + public function teardown() { + } + +} + +class Database_For_Test extends Database { + static function instance() { + $db = new Database_For_Test(); + $db->_table_names["{items}"] = "g3test_items"; + $db->config["table_prefix"] = "g3test_"; + return $db; + } +} diff --git a/modules/gallery/tests/Dir_Helper_Test.php b/modules/gallery/tests/Dir_Helper_Test.php new file mode 100644 index 00000000..46bb871c --- /dev/null +++ b/modules/gallery/tests/Dir_Helper_Test.php @@ -0,0 +1,32 @@ +assert_boolean(!file_exists($filename), "File not deleted"); + $this->assert_boolean(!file_exists($dirname), "Directory not deleted"); + } +} diff --git a/modules/gallery/tests/DrawForm_Test.php b/modules/gallery/tests/DrawForm_Test.php new file mode 100644 index 00000000..2c5aaba4 --- /dev/null +++ b/modules/gallery/tests/DrawForm_Test.php @@ -0,0 +1,84 @@ + "gTestGroupForm")); + $form->input("title")->label(t("Title")); + $form->textarea("description")->label(t("Text Area")); + $form->submit("")->value(t("Submit")); + $rendered = $form->__toString(); + + $expected = "
    \n" . + "\n" . + "
      \n" . + "
    • \n" . + " \n" . + " \n" . + "
    • \n" . + "
    • \n" . + " \n" . + " \n" . + "
    • \n" . + "
    • \n" . + " \n" . + "
    • \n" . + "
    \n" . + "
    \n"; + $this->assert_same($expected, $rendered); + } + + function group_test() { + $form = new Forge("test/controller", "", "post", array("id" => "gTestGroupForm")); + $group = $form->group("test_group")->label(t("Test Group")); + $group->input("title")->label(t("Title")); + $group->textarea("description")->label(t("Text Area")); + $group->submit("")->value(t("Submit")); + $rendered = $form->__toString(); + + $expected = "
    \n" . + "\n" . + "
    \n" . + " Test Group\n" . + "
      \n" . + "
    • \n" . + " \n" . + " \n" . + "
    • \n" . + "
    • \n" . + " \n" . + " \n" . + "
    • \n" . + "
    • \n" . + " \n" . + "
    • \n" . + "
    \n" . + "
    \n" . + "
    \n"; + $this->assert_same($expected, $rendered); + } + +} + diff --git a/modules/gallery/tests/File_Structure_Test.php b/modules/gallery/tests/File_Structure_Test.php new file mode 100644 index 00000000..1caa82ba --- /dev/null +++ b/modules/gallery/tests/File_Structure_Test.php @@ -0,0 +1,235 @@ +getPathname())) { + $this->assert_false( + preg_match('/\?\>\s*$/', file_get_contents($file)), + "{$file->getPathname()} ends in ?>"); + } + } + } + + public function view_files_correct_suffix_test() { + $dir = new GalleryCodeFilterIterator( + new RecursiveIteratorIterator(new RecursiveDirectoryIterator(DOCROOT))); + foreach ($dir as $file) { + if (strpos($file, "views")) { + $this->assert_true( + preg_match("#/views/.*?(\.html|mrss|txt)\.php$#", $file->getPathname()), + "{$file->getPathname()} should end in .{html,mrss,txt}.php"); + } + } + } + + public function no_windows_line_endings_test() { + $dir = new GalleryCodeFilterIterator( + new RecursiveIteratorIterator(new RecursiveDirectoryIterator(DOCROOT))); + foreach ($dir as $file) { + if (preg_match("/\.(php|css|html|js)$/", $file)) { + foreach (file($file) as $line) { + $this->assert_true(substr($line, -2) != "\r\n", "$file has windows style line endings"); + } + } + } + } + + private function _check_view_preamble($path, &$errors) { + // The preamble for views is a single line that prevents direct script access + if (strpos($path, SYSPATH) === 0) { + // Kohana preamble + $expected = "\n"; + } else { + // Gallery preamble + // @todo use the same preamble for both! + $expected = "\n"; + } + + $fp = fopen($path, "r"); + $actual = fgets($fp); + fclose($fp); + + if ($expected != $actual) { + $errors[] = "$path:1\n expected:\n\t$expected\n actual:\n\t$actual"; + } + } + + private function _check_php_preamble($path, &$errors) { + if (strpos($path, SYSPATH) === 0 || + strpos($path, MODPATH . "unit_test") === 0) { + // Kohana: we only care about the first line + $fp = fopen($path, "r"); + $actual = array(fgets($fp)); + fclose($fp); + $expected = array("_get_preamble($path); + $expected = array( + "getPathname(); + switch ($path) { + case DOCROOT . "installer/database_config.php": + case DOCROOT . "installer/init_var.php": + // Special case views + $this->_check_view_preamble($path, $errors); + break; + + case DOCROOT . "index.php": + case DOCROOT . "installer/index.php": + // Front controllers + break; + + case DOCROOT . "index.local.php": + // Special case optional file, not part of the codebase + break; + + default: + if (strpos($path, DOCROOT . "var/logs") === 0) { + continue; + } else if (preg_match("/views/", $path)) { + $this->_check_view_preamble($path, $errors); + } else { + $this->_check_php_preamble($path, $errors); + } + } + } + + if ($errors) { + $this->assert_false(true, "Preamble errors:\n" . join("\n", $errors)); + } + } + + public function no_tabs_in_our_code_test() { + $dir = new PhpCodeFilterIterator( + new GalleryCodeFilterIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(DOCROOT)))); + foreach ($dir as $file) { + $this->assert_false( + preg_match('/\t/', file_get_contents($file)), + "{$file->getPathname()} has tabs in it"); + } + } + + private function _get_preamble($file) { + $lines = file($file); + $copy = array(); + for ($i = 0; $i < count($lines); $i++) { + $copy[] = rtrim($lines[$i]); + if (!strncmp($lines[$i], ' */', 3)) { + return $copy; + } + } + return $copy; + } + + public function helpers_are_static_test() { + $dir = new PhpCodeFilterIterator( + new GalleryCodeFilterIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(DOCROOT)))); + foreach ($dir as $file) { + if (basename(dirname($file)) == "helpers") { + foreach (file($file) as $line) { + $this->assert_true( + !preg_match("/\sfunction\s.*\(/", $line) || + preg_match("/^\s*(private static function _|static function)/", $line), + "should be \"static function foo\" or \"private static function _foo\":\n" . + "$file\n$line\n"); + } + } + } + } +} + +class PhpCodeFilterIterator extends FilterIterator { + public function accept() { + $path_name = $this->getInnerIterator()->getPathName(); + return (substr($path_name, -4) == ".php" && + !(strpos($path_name, VARPATH) === 0)); + } +} + +class GalleryCodeFilterIterator extends FilterIterator { + public function accept() { + // Skip anything that we didn"t write + $path_name = $this->getInnerIterator()->getPathName(); + return !( + strpos($path_name, ".svn") || + strpos($path_name, "core/views/kohana_profiler.php") !== false || + strpos($path_name, DOCROOT . "test") !== false || + strpos($path_name, DOCROOT . "var") !== false || + strpos($path_name, MODPATH . "forge") !== false || + strpos($path_name, APPPATH . "views/kohana_error_page.php") !== false || + strpos($path_name, MODPATH . "gallery_unit_test/views/kohana_error_page.php") !== false || + strpos($path_name, MODPATH . "gallery_unit_test/views/kohana_unit_test_cli.php") !== false || + strpos($path_name, MODPATH . "unit_test") !== false || + strpos($path_name, MODPATH . "exif/lib") !== false || + strpos($path_name, MODPATH . "user/lib/PasswordHash") !== false || + strpos($path_name, DOCROOT . "lib/swfupload") !== false || + strpos($path_name, SYSPATH) !== false || + substr($path_name, -1, 1) == "~"); + } +} diff --git a/modules/gallery/tests/I18n_Test.php b/modules/gallery/tests/I18n_Test.php new file mode 100644 index 00000000..9010606a --- /dev/null +++ b/modules/gallery/tests/I18n_Test.php @@ -0,0 +1,108 @@ + 'en', + 'default_locale' => 'te_ST', + 'locale_dir' => VARPATH . 'locale/'); + $this->i18n = I18n::instance($config); + + ORM::factory("incoming_translation") + ->where("locale", "te_ST") + ->delete_all(); + + $messages_te_ST = array( + array('Hello world', 'Hallo Welt'), + array(array('one' => 'One item has been added', + 'other' => '%count elements have been added'), + array('one' => 'Ein Element wurde hinzugefuegt.', + 'other' => '%count Elemente wurden hinzugefuegt.')), + array('Hello %name, how are you today?', 'Hallo %name, wie geht es Dir heute?')); + + foreach ($messages_te_ST as $data) { + list ($message, $translation) = $data; + $entry = ORM::factory("incoming_translation"); + $entry->key = I18n::get_message_key($message); + $entry->message = serialize($message); + $entry->translation = serialize($translation); + $entry->locale = 'te_ST'; + $entry->revision = null; + $entry->save(); + } + } + + public function get_locale_test() { + $locale = $this->i18n->locale(); + $this->assert_equal("te_ST", $locale); + } + + public function set_locale_test() { + $this->i18n->locale("de_DE"); + $locale = $this->i18n->locale(); + $this->assert_equal("de_DE", $locale); + } + + public function translate_simple_test() { + $result = $this->i18n->translate('Hello world'); + $this->assert_equal('Hallo Welt', $result); + } + + public function translate_simple_root_fallback_test() { + $result = $this->i18n->translate('Hello world zzz'); + $this->assert_equal('Hello world zzz', $result); + } + + public function translate_plural_other_test() { + $result = $this->i18n->translate(array('one' => 'One item has been added', + 'other' => '%count elements have been added'), + array('count' => 5)); + $this->assert_equal('5 Elemente wurden hinzugefuegt.', $result); + } + + public function translate_plural_one_test() { + $result = $this->i18n->translate(array('one' => 'One item has been added', + 'other' => '%count elements have been added'), + array('count' => 1)); + $this->assert_equal('Ein Element wurde hinzugefuegt.', $result); + } + + public function translate_interpolate_test() { + $result = $this->i18n->translate('Hello %name, how are you today?', array('name' => 'John')); + $this->assert_equal('Hallo John, wie geht es Dir heute?', $result); + } + + public function translate_interpolate_missing_value_test() { + $result = $this->i18n->translate('Hello %name, how are you today?', array('foo' => 'bar')); + $this->assert_equal('Hallo %name, wie geht es Dir heute?', $result); + } + + public function translate_plural_zero_test() { + // te_ST has the same plural rules as en and de. + // For count 0, plural form "other" should be used. + $result = $this->i18n->translate(array('one' => 'One item has been added', + 'other' => '%count elements have been added'), + array('count' => 0)); + $this->assert_equal('0 Elemente wurden hinzugefuegt.', $result); + } +} \ No newline at end of file diff --git a/modules/gallery/tests/Item_Model_Test.php b/modules/gallery/tests/Item_Model_Test.php new file mode 100644 index 00000000..615b8997 --- /dev/null +++ b/modules/gallery/tests/Item_Model_Test.php @@ -0,0 +1,143 @@ +assert_true(!empty($item->created)); + $this->assert_true(!empty($item->updated)); + } + + private function create_random_item() { + $item = ORM::factory("item"); + /* Set all required fields (values are irrelevant) */ + $item->name = rand(); + $item->type = "photo"; + return $item->add_to_parent(ORM::factory("item", 1)); + } + + public function updating_doesnt_change_created_date_test() { + $item = self::create_random_item(); + + // Force the creation date to something well known + $db = Database::instance(); + $db->update("items", array("created" => 0, "updated" => 0), array("id" => $item->id)); + $item->reload(); + $item->title = "foo"; // force a change + $item->save(); + + $this->assert_true(empty($item->created)); + $this->assert_true(!empty($item->updated)); + } + + public function updating_view_count_only_doesnt_change_updated_date_test() { + $item = self::create_random_item(); + $item->reload(); + $this->assert_same(0, $item->view_count); + + // Force the updated date to something well known + $db = Database::instance(); + $db->update("items", array("updated" => 0), array("id" => $item->id)); + $item->reload(); + $item->view_count++; + $item->save(); + + $this->assert_same(1, $item->view_count); + $this->assert_true(empty($item->updated)); + } + + public function move_photo_test() { + // Create a test photo + $item = self::create_random_item(); + + file_put_contents($item->thumb_path(), "thumb"); + file_put_contents($item->resize_path(), "resize"); + file_put_contents($item->file_path(), "file"); + + $original_name = $item->name; + $new_name = rand(); + + // Now rename it + $item->rename($new_name)->save(); + + // Expected: the name changed, the name is now baked into all paths, and all files were moved. + $this->assert_equal($new_name, $item->name); + $this->assert_equal($new_name, basename($item->file_path())); + $this->assert_equal($new_name, basename($item->thumb_path())); + $this->assert_equal($new_name, basename($item->resize_path())); + $this->assert_equal("thumb", file_get_contents($item->thumb_path())); + $this->assert_equal("resize", file_get_contents($item->resize_path())); + $this->assert_equal("file", file_get_contents($item->file_path())); + } + + public function move_album_test() { + // Create an album with a photo in it + $root = ORM::factory("item", 1); + $album = album::create($root, rand(), rand(), rand()); + $photo = ORM::factory("item"); + $photo->name = rand(); + $photo->type = "photo"; + $photo->add_to_parent($album); + + file_put_contents($photo->thumb_path(), "thumb"); + file_put_contents($photo->resize_path(), "resize"); + file_put_contents($photo->file_path(), "file"); + + $original_album_name = $album->name; + $original_photo_name = $photo->name; + $new_album_name = rand(); + + // Now rename the album + $album->rename($new_album_name)->save(); + $photo->reload(); + + // Expected: + // * the album name changed. + // * the album dirs are all moved + // * the photo's paths are all inside the albums paths + // * the photo files are all still intact and accessible + $this->assert_equal($new_album_name, $album->name); + $this->assert_equal($new_album_name, basename($album->file_path())); + $this->assert_equal($new_album_name, basename(dirname($album->thumb_path()))); + $this->assert_equal($new_album_name, basename(dirname($album->resize_path()))); + + $this->assert_same(0, strpos($photo->file_path(), $album->file_path())); + $this->assert_same(0, strpos($photo->thumb_path(), dirname($album->thumb_path()))); + $this->assert_same(0, strpos($photo->resize_path(), dirname($album->resize_path()))); + + $this->assert_equal("thumb", file_get_contents($photo->thumb_path())); + $this->assert_equal("resize", file_get_contents($photo->resize_path())); + $this->assert_equal("file", file_get_contents($photo->file_path())); + } + + public function item_rename_wont_accept_slash_test() { + // Create a test photo + $item = self::create_random_item(); + + $new_name = rand() . "/"; + + try { + $item->rename($new_name)->save(); + } catch (Exception $e) { + // pass + return; + } + $this->assert_false(true, "Item_Model::rename should not accept / characters"); + } +} diff --git a/modules/gallery/tests/Menu_Test.php b/modules/gallery/tests/Menu_Test.php new file mode 100644 index 00000000..c91aee0b --- /dev/null +++ b/modules/gallery/tests/Menu_Test.php @@ -0,0 +1,32 @@ +append(Menu::factory("link")->id("element_1")) + ->append(Menu::factory("dialog")->id("element_2")) + ->append(Menu::factory("submenu")->id("element_3") + ->append(Menu::factory("link")->id("element_3_1"))); + + $this->assert_equal("element_2", $menu->get("element_2")->id); + $this->assert_equal("element_3_1", $menu->get("element_3")->get("element_3_1")->id); + } +} \ No newline at end of file diff --git a/modules/gallery/tests/Movie_Helper_Test.php b/modules/gallery/tests/Movie_Helper_Test.php new file mode 100644 index 00000000..b92ef3f8 --- /dev/null +++ b/modules/gallery/tests/Movie_Helper_Test.php @@ -0,0 +1,46 @@ +assert_true(false, "Shouldn't create a movie with / in the name"); + } + + public function create_movie_shouldnt_allow_names_with_trailing_periods_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + try { + $movie = movie::create($root, DOCROOT . "core/tests/test.jpg", "$rand.jpg.", $rand, $rand); + } catch (Exception $e) { + $this->assert_equal("@todo NAME_CANNOT_END_IN_PERIOD", $e->getMessage()); + return; + } + + $this->assert_true(false, "Shouldn't create a movie with trailing . in the name"); + } +} diff --git a/modules/gallery/tests/ORM_MPTT_Test.php b/modules/gallery/tests/ORM_MPTT_Test.php new file mode 100644 index 00000000..200c8a74 --- /dev/null +++ b/modules/gallery/tests/ORM_MPTT_Test.php @@ -0,0 +1,221 @@ +type = "album"; + $album->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); + $album->sort_column = "weight"; + $album->sort_order = "ASC"; + $album->add_to_parent($root); + + $this->assert_equal($album->parent()->right - 2, $album->left); + $this->assert_equal($album->parent()->right - 1, $album->right); + $this->assert_equal($album->parent()->level + 1, $album->level); + $this->assert_equal($album->parent()->id, $album->parent_id); + } + + public function add_hierarchy_test() { + $root = ORM::factory("item", 1); + $album1 = self::create_item_and_add_to_parent($root); + $album1_1 = self::create_item_and_add_to_parent($album1); + $album1_2 = self::create_item_and_add_to_parent($album1); + $album1_1_1 = self::create_item_and_add_to_parent($album1_1); + $album1_1_2 = self::create_item_and_add_to_parent($album1_1); + + $album1->reload(); + $this->assert_equal(9, $album1->right - $album1->left); + + $album1_1->reload(); + $this->assert_equal(5, $album1_1->right - $album1_1->left); + } + + public function delete_hierarchy_test() { + $root = ORM::factory("item", 1); + $album1 = self::create_item_and_add_to_parent($root); + $album1_1 = self::create_item_and_add_to_parent($album1); + $album1_2 = self::create_item_and_add_to_parent($album1); + $album1_1_1 = self::create_item_and_add_to_parent($album1_1); + $album1_1_2 = self::create_item_and_add_to_parent($album1_1); + + $album1_1->delete(); + $album1->reload(); + + // Now album1 contains only album1_2 + $this->assert_equal(3, $album1->right - $album1->left); + } + + public function move_to_test() { + $root = ORM::factory("item", 1); + $album1 = album::create($root, "move_to_test_1", "move_to_test_1"); + $album1_1 = album::create($album1, "move_to_test_1_1", "move_to_test_1_1"); + $album1_2 = album::create($album1, "move_to_test_1_2", "move_to_test_1_2"); + $album1_1_1 = album::create($album1_1, "move_to_test_1_1_1", "move_to_test_1_1_1"); + $album1_1_2 = album::create($album1_1, "move_to_test_1_1_2", "move_to_test_1_1_2"); + + $album1_2->reload(); + $album1_1_1->reload(); + + $album1_1_1->move_to($album1_2); + + $album1_1->reload(); + $album1_2->reload(); + + $this->assert_equal(3, $album1_1->right - $album1_1->left); + $this->assert_equal(3, $album1_2->right - $album1_2->left); + + $this->assert_equal( + array($album1_1_2->id => "move_to_test_1_1_2"), + $album1_1->children()->select_list()); + + $this->assert_equal( + array($album1_1_1->id => "move_to_test_1_1_1"), + $album1_2->children()->select_list()); + } + + public function parent_test() { + $root = ORM::factory("item", 1); + $album = self::create_item_and_add_to_parent($root); + + $parent = ORM::factory("item", 1); + $this->assert_equal($parent->id, $album->parent()->id); + } + + public function parents_test() { + $root = ORM::factory("item", 1); + $outer = self::create_item_and_add_to_parent($root); + $inner = self::create_item_and_add_to_parent($outer); + + $parent_ids = array(); + foreach ($inner->parents() as $parent) { + $parent_ids[] = $parent->id; + } + $this->assert_equal(array(1, $outer->id), $parent_ids); + } + + public function children_test() { + $root = ORM::factory("item", 1); + $outer = self::create_item_and_add_to_parent($root); + $inner1 = self::create_item_and_add_to_parent($outer); + $inner2 = self::create_item_and_add_to_parent($outer); + + $child_ids = array(); + foreach ($outer->children() as $child) { + $child_ids[] = $child->id; + } + $this->assert_equal(array($inner1->id, $inner2->id), $child_ids); + } + + public function children_limit_test() { + $root = ORM::factory("item", 1); + $outer = self::create_item_and_add_to_parent($root); + $inner1 = self::create_item_and_add_to_parent($outer); + $inner2 = self::create_item_and_add_to_parent($outer); + + $this->assert_equal(array($inner2->id => $inner2->name), + $outer->children(1, 1)->select_list('id')); + } + + public function children_count_test() { + $root = ORM::factory("item", 1); + $outer = self::create_item_and_add_to_parent($root); + $inner1 = self::create_item_and_add_to_parent($outer); + $inner2 = self::create_item_and_add_to_parent($outer); + + $this->assert_equal(2, $outer->children_count()); + } + + public function descendant_test() { + $root = ORM::factory("item", 1); + + $parent = ORM::factory("item"); + $parent->type = "album"; + $parent->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); + $parent->sort_column = "weight"; + $parent->sort_order = "ASC"; + $parent->add_to_parent($root); + + $photo = ORM::factory("item"); + $photo->type = "photo"; + $photo->add_to_parent($parent); + + $album1 = ORM::factory("item"); + $album1->type = "album"; + $album1->rand_key = ((float)mt_rand()) / (float)mt_getrandmax(); + $album1->sort_column = "weight"; + $album1->sort_order = "ASC"; + $album1->add_to_parent($parent); + + $photo1 = ORM::factory("item"); + $photo1->type = "photo"; + $photo1->add_to_parent($album1); + + $parent->reload(); + + $this->assert_equal(3, $parent->descendants()->count()); + $this->assert_equal(2, $parent->descendants(null, 0, "photo")->count()); + $this->assert_equal(1, $parent->descendants(null, 0, "album")->count()); + } + + public function descendant_limit_test() { + $root = ORM::factory("item", 1); + + $parent = self::create_item_and_add_to_parent($root); + $album1 = self::create_item_and_add_to_parent($parent); + $album2 = self::create_item_and_add_to_parent($parent); + $album3 = self::create_item_and_add_to_parent($parent); + + $parent->reload(); + $this->assert_equal(2, $parent->descendants(2)->count()); + } + + public function descendant_count_test() { + $root = ORM::factory("item", 1); + + $parent = ORM::factory("item"); + $parent->type = "album"; + $parent->add_to_parent($root); + + $photo = ORM::factory("item"); + $photo->type = "photo"; + $photo->add_to_parent($parent); + + $album1 = ORM::factory("item"); + $album1->type = "album"; + $album1->add_to_parent($parent); + + $photo1 = ORM::factory("item"); + $photo1->type = "photo"; + $photo1->add_to_parent($album1); + + $parent->reload(); + + $this->assert_equal(3, $parent->descendants_count()); + $this->assert_equal(2, $parent->descendants_count("photo")); + $this->assert_equal(1, $parent->descendants_count("album")); + } +} diff --git a/modules/gallery/tests/Photo_Helper_Test.php b/modules/gallery/tests/Photo_Helper_Test.php new file mode 100644 index 00000000..deb11bb9 --- /dev/null +++ b/modules/gallery/tests/Photo_Helper_Test.php @@ -0,0 +1,109 @@ +assert_equal(VARPATH . "albums/$rand.jpg", $photo->file_path()); + $this->assert_equal(VARPATH . "thumbs/{$rand}.jpg", $photo->thumb_path()); + $this->assert_equal(VARPATH . "resizes/{$rand}.jpg", $photo->resize_path()); + + $this->assert_true(is_file($photo->file_path()), "missing: {$photo->file_path()}"); + $this->assert_true(is_file($photo->resize_path()), "missing: {$photo->resize_path()}"); + $this->assert_true(is_file($photo->thumb_path()), "missing: {$photo->thumb_path()}"); + + $this->assert_equal($root->id, $photo->parent_id); // MPTT tests cover other hierarchy checks + $this->assert_equal("$rand.jpg", $photo->name); + $this->assert_equal($rand, $photo->title); + $this->assert_equal($rand, $photo->description); + $this->assert_equal("image/jpeg", $photo->mime_type); + $this->assert_equal($image_info[0], $photo->width); + $this->assert_equal($image_info[1], $photo->height); + + $this->assert_equal($photo->parent()->right - 2, $photo->left); + $this->assert_equal($photo->parent()->right - 1, $photo->right); + } + + public function create_conflicting_photo_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $photo1 = photo::create($root, DOCROOT . "core/tests/test.jpg", "$rand.jpg", $rand, $rand); + $photo2 = photo::create($root, DOCROOT . "core/tests/test.jpg", "$rand.jpg", $rand, $rand); + $this->assert_true($photo1->name != $photo2->name); + } + + public function create_photo_with_no_extension_test() { + $root = ORM::factory("item", 1); + try { + photo::create($root, "/tmp", "name", "title", "description"); + $this->assert_false("should fail with an exception"); + } catch (Exception $e) { + // pass + } + } + + public function thumb_url_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $photo = photo::create($root, DOCROOT . "core/tests/test.jpg", "$rand.jpg", $rand, $rand); + $this->assert_equal("http://./var/thumbs/{$rand}.jpg", $photo->thumb_url()); + } + + public function resize_url_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + $album = album::create($root, $rand, $rand, $rand); + $photo = photo::create($album, DOCROOT . "core/tests/test.jpg", "$rand.jpg", $rand, $rand); + + $this->assert_equal("http://./var/resizes/{$rand}/{$rand}.jpg", $photo->resize_url()); + } + + public function create_photo_shouldnt_allow_names_with_slash_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + try { + $photo = photo::create($root, DOCROOT . "core/tests/test.jpg", "$rand/.jpg", $rand, $rand); + } catch (Exception $e) { + // pass + return; + } + + $this->assert_true(false, "Shouldn't create a photo with / in the name"); + } + + public function create_photo_silently_trims_trailing_periods_test() { + $rand = rand(); + $root = ORM::factory("item", 1); + try { + $photo = photo::create($root, DOCROOT . "core/tests/test.jpg", "$rand.jpg.", $rand, $rand); + } catch (Exception $e) { + $this->assert_equal("@todo NAME_CANNOT_END_IN_PERIOD", $e->getMessage()); + return; + } + + $this->assert_true(false, "Shouldn't create a photo with trailing . in the name"); + } +} diff --git a/modules/gallery/tests/Photos_Controller_Test.php b/modules/gallery/tests/Photos_Controller_Test.php new file mode 100644 index 00000000..71319315 --- /dev/null +++ b/modules/gallery/tests/Photos_Controller_Test.php @@ -0,0 +1,74 @@ +_post = $_POST; + } + + public function teardown() { + $_POST = $this->_post; + } + + public function change_photo_test() { + $controller = new Photos_Controller(); + $root = ORM::factory("item", 1); + $photo = photo::create($root, DOCROOT . "core/tests/test.jpg", "test.jpeg", "test", "test"); + $orig_name = $photo->name; + + $_POST["filename"] = "test.jpeg"; + $_POST["name"] = "new name"; + $_POST["title"] = "new title"; + $_POST["description"] = "new description"; + $_POST["csrf"] = access::csrf_token(); + access::allow(group::everybody(), "edit", $root); + + ob_start(); + $controller->_update($photo); + $results = ob_get_contents(); + ob_end_clean(); + + $this->assert_equal( + json_encode(array("result" => "success", + "location" => "http://./index.php/test.jpeg")), + $results); + $this->assert_equal("new title", $photo->title); + $this->assert_equal("new description", $photo->description); + + // We don't change the name, yet. + $this->assert_equal($orig_name, $photo->name); + } + + public function change_photo_no_csrf_fails_test() { + $controller = new Photos_Controller(); + $root = ORM::factory("item", 1); + $photo = photo::create($root, DOCROOT . "core/tests/test.jpg", "test", "test", "test"); + $_POST["name"] = "new name"; + $_POST["title"] = "new title"; + $_POST["description"] = "new description"; + access::allow(group::everybody(), "edit", $root); + + try { + $controller->_update($photo); + $this->assert_true(false, "This should fail"); + } catch (Exception $e) { + // pass + } + } +} diff --git a/modules/gallery/tests/REST_Controller_Test.php b/modules/gallery/tests/REST_Controller_Test.php new file mode 100644 index 00000000..8fb04d86 --- /dev/null +++ b/modules/gallery/tests/REST_Controller_Test.php @@ -0,0 +1,197 @@ +_post = $_POST; + $this->mock_controller = new Mock_RESTful_Controller("mock"); + $this->mock_not_loaded_controller = new Mock_RESTful_Controller("mock_not_loaded"); + $_POST = array(); + } + + public function teardown() { + $_POST = $this->_post; + } + + public function dispatch_index_test() { + $_SERVER["REQUEST_METHOD"] = "GET"; + $_POST["_method"] = ""; + $this->mock_controller->__call("index", ""); + $this->assert_equal("index", $this->mock_controller->method_called); + } + + public function dispatch_show_test() { + $_SERVER["REQUEST_METHOD"] = "GET"; + $_POST["_method"] = ""; + $this->mock_controller->__call("3", ""); + $this->assert_equal("show", $this->mock_controller->method_called); + $this->assert_equal("Mock_Model", get_class($this->mock_controller->resource)); + } + + public function dispatch_update_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = "PUT"; + $_POST["csrf"] = access::csrf_token(); + $this->mock_controller->__call("3", ""); + $this->assert_equal("update", $this->mock_controller->method_called); + $this->assert_equal("Mock_Model", get_class($this->mock_controller->resource)); + } + + public function dispatch_update_fails_without_csrf_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = "PUT"; + try { + $this->mock_controller->__call("3", ""); + $this->assert_false(true, "this should fail with a forbidden exception"); + } catch (Exception $e) { + // pass + } + } + + public function dispatch_delete_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = "DELETE"; + $_POST["csrf"] = access::csrf_token(); + $this->mock_controller->__call("3", ""); + $this->assert_equal("delete", $this->mock_controller->method_called); + $this->assert_equal("Mock_Model", get_class($this->mock_controller->resource)); + } + + public function dispatch_delete_fails_without_csrf_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = "DELETE"; + try { + $this->mock_controller->__call("3", ""); + $this->assert_false(true, "this should fail with a forbidden exception"); + } catch (Exception $e) { + // pass + } + } + + public function dispatch_404_test() { + /* The dispatcher should throw a 404 if the resource isn't loaded and the method isn't POST. */ + $methods = array( + array("GET", ""), + array("POST", "PUT"), + array("POST", "DELETE")); + + foreach ($methods as $method) { + $_SERVER["REQUEST_METHOD"] = $method[0]; + $_POST["_method"] = $method[1]; + $exception_caught = false; + try { + $this->mock_not_loaded_controller->__call(rand(), ""); + } catch (Kohana_404_Exception $e) { + $exception_caught = true; + } + $this->assert_true($exception_caught, "$method[0], $method[1]"); + } + } + + public function dispatch_create_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = ""; + $_POST["csrf"] = access::csrf_token(); + $this->mock_not_loaded_controller->__call("", ""); + $this->assert_equal("create", $this->mock_not_loaded_controller->method_called); + $this->assert_equal( + "Mock_Not_Loaded_Model", get_class($this->mock_not_loaded_controller->resource)); + } + + public function dispatch_create_fails_without_csrf_test() { + $_SERVER["REQUEST_METHOD"] = "POST"; + $_POST["_method"] = ""; + try { + $this->mock_not_loaded_controller->__call("", ""); + $this->assert_false(true, "this should fail with a forbidden exception"); + } catch (Exception $e) { + // pass + } + } + + public function dispatch_form_test_add() { + $this->mock_controller->form_add("args"); + $this->assert_equal("form_add", $this->mock_controller->method_called); + $this->assert_equal("args", $this->mock_controller->resource); + } + + public function dispatch_form_test_edit() { + $this->mock_controller->form_edit("1"); + $this->assert_equal("form_edit", $this->mock_controller->method_called); + $this->assert_equal("Mock_Model", get_class($this->mock_controller->resource)); + } + + public function routes_test() { + $this->assert_equal("mock/form_add/args", router::routed_uri("form/add/mock/args")); + $this->assert_equal("mock/form_edit/args", router::routed_uri("form/edit/mock/args")); + $this->assert_equal(null, router::routed_uri("rest/args")); + } +} + +class Mock_RESTful_Controller extends REST_Controller { + public $method_called; + public $resource; + + public function __construct($type) { + $this->resource_type = $type; + parent::__construct(); + } + + public function _index() { + $this->method_called = "index"; + } + + public function _create($resource) { + $this->method_called = "create"; + $this->resource = $resource; + } + + public function _show($resource) { + $this->method_called = "show"; + $this->resource = $resource; + } + + public function _update($resource) { + $this->method_called = "update"; + $this->resource = $resource; + } + + public function _delete($resource) { + $this->method_called = "delete"; + $this->resource = $resource; + } + + public function _form_add($args) { + $this->method_called = "form_add"; + $this->resource = $args; + } + + public function _form_edit($resource) { + $this->method_called = "form_edit"; + $this->resource = $resource; + } +} + +class Mock_Model { + public $loaded = true; +} + +class Mock_Not_Loaded_Model { + public $loaded = false; +} diff --git a/modules/gallery/tests/REST_Helper_Test.php b/modules/gallery/tests/REST_Helper_Test.php new file mode 100644 index 00000000..1bfc63ab --- /dev/null +++ b/modules/gallery/tests/REST_Helper_Test.php @@ -0,0 +1,45 @@ +_post = $_POST; + } + + public function teardown() { + $_POST = $this->_post; + } + + public function request_method_test() { + foreach (array("GET", "POST") as $method) { + foreach (array("", "PUT", "DELETE") as $tunnel) { + if ($method == "GET") { + $expected = "GET"; + } else { + $expected = $tunnel == "" ? $method : $tunnel; + } + $_SERVER["REQUEST_METHOD"] = $method; + $_POST["_method"] = $tunnel; + + $this->assert_equal(strtolower(rest::request_method()), strtolower($expected), + "Request method: {$method}, tunneled: {$tunnel}"); + } + } + } +} diff --git a/modules/gallery/tests/Sendmail_Test.php b/modules/gallery/tests/Sendmail_Test.php new file mode 100644 index 00000000..64c1fff0 --- /dev/null +++ b/modules/gallery/tests/Sendmail_Test.php @@ -0,0 +1,115 @@ +to("receiver@someemail.com") + /* + * @todo figure out why this test fails so badly, when the following + * line is not supplied. It doesn't seem to be set by setup method + * as you would expect. + */ + ->from("from@gallery3.com") + ->subject("Test Email Unit test") + ->message("The mail message body") + ->send() + ->send_text; + + $this->assert_equal($expected, $result); + } + + public function sendmail_reply_to_test() { + $expected = "To: receiver@someemail.com\r\n" . + "From: from@gallery3.com\n" . + "Reply-To: reply-to@gallery3.com\r\n" . + "Subject: Test Email Unit test\r\n\r\n" . + "The mail message body"; + $result = Sendmail_For_Test::factory() + ->to("receiver@someemail.com") + ->subject("Test Email Unit test") + ->reply_to("reply-to@gallery3.com") + ->message("The mail message body") + ->send() + ->send_text; + $this->assert_equal($expected, $result); + } + + public function sendmail_html_message_test() { + $expected = "To: receiver@someemail.com\r\n" . + "From: from@gallery3.com\n" . + "Reply-To: public@gallery3.com\n" . + "MIME-Version: 1.0\n" . + "Content-type: text/html; charset=iso-8859-1\r\n" . + "Subject: Test Email Unit test\r\n\r\n" . + "

    This is an html msg

    "; + $result = Sendmail_For_Test::factory() + ->to("receiver@someemail.com") + ->subject("Test Email Unit test") + ->header("MIME-Version", "1.0") + ->header("Content-type", "text/html; charset=iso-8859-1") + ->message("

    This is an html msg

    ") + ->send() + ->send_text; + $this->assert_equal($expected, $result); + } + + public function sendmail_wrapped_message_test() { + $expected = "To: receiver@someemail.com\r\n" . + "From: from@gallery3.com\n" . + "Reply-To: public@gallery3.com\r\n" . + "Subject: Test Email Unit test\r\n\r\n" . + "This is a long message that needs to go\n" . + "over forty characters If we get lucky we\n" . + "might make it long enought to wrap a\n" . + "couple of times."; + $result = Sendmail_For_Test::factory() + ->to("receiver@someemail.com") + ->subject("Test Email Unit test") + ->line_length(40) + ->message("This is a long message that needs to go over forty characters " . + "If we get lucky we might make it long enought to wrap a couple " . + "of times.") + ->send() + ->send_text; + $this->assert_equal($expected, $result); + } +} + +class Sendmail_For_Test extends Sendmail { + static function factory() { + return new Sendmail_For_Test(); + } + + public function mail($to, $subject, $message, $headers) { + $this->send_text = "To: $to\r\n{$headers}\r\nSubject: $this->subject\r\n\r\n$message"; + return true; + } +} \ No newline at end of file diff --git a/modules/gallery/tests/Var_Test.php b/modules/gallery/tests/Var_Test.php new file mode 100644 index 00000000..82370631 --- /dev/null +++ b/modules/gallery/tests/Var_Test.php @@ -0,0 +1,49 @@ +assert_equal("original value", module::get_var("core", "Parameter")); + + module::set_var("core", "Parameter", "updated value"); + $this->assert_equal("updated value", module::get_var("core", "Parameter")); + } + + public function clear_parameter_test() { + module::set_var("core", "Parameter", "original value"); + $this->assert_equal("original value", module::get_var("core", "Parameter")); + + module::clear_var("core", "Parameter"); + $this->assert_equal(null, module::get_var("core", "Parameter")); + } + + public function incr_parameter_test() { + module::set_var("core", "Parameter", "original value"); + module::incr_var("core", "Parameter"); + $this->assert_equal("1", module::get_var("core", "Parameter")); + + module::set_var("core", "Parameter", "2"); + module::incr_var("core", "Parameter", "9"); + $this->assert_equal("11", module::get_var("core", "Parameter")); + + module::incr_var("core", "NonExistent", "9"); + $this->assert_equal(null, module::get_var("core", "NonExistent")); + } +} \ No newline at end of file diff --git a/modules/gallery/tests/images/DSC_0003.jpg b/modules/gallery/tests/images/DSC_0003.jpg new file mode 100644 index 00000000..5780d9d8 Binary files /dev/null and b/modules/gallery/tests/images/DSC_0003.jpg differ diff --git a/modules/gallery/tests/images/DSC_0005.jpg b/modules/gallery/tests/images/DSC_0005.jpg new file mode 100644 index 00000000..4d2b53a9 Binary files /dev/null and b/modules/gallery/tests/images/DSC_0005.jpg differ diff --git a/modules/gallery/tests/images/DSC_0017.jpg b/modules/gallery/tests/images/DSC_0017.jpg new file mode 100644 index 00000000..b7f7bb90 Binary files /dev/null and b/modules/gallery/tests/images/DSC_0017.jpg differ diff --git a/modules/gallery/tests/images/DSC_0019.jpg b/modules/gallery/tests/images/DSC_0019.jpg new file mode 100644 index 00000000..0ce25aa4 Binary files /dev/null and b/modules/gallery/tests/images/DSC_0019.jpg differ diff --git a/modules/gallery/tests/images/DSC_0067.jpg b/modules/gallery/tests/images/DSC_0067.jpg new file mode 100644 index 00000000..84f134cb Binary files /dev/null and b/modules/gallery/tests/images/DSC_0067.jpg differ diff --git a/modules/gallery/tests/images/DSC_0072.jpg b/modules/gallery/tests/images/DSC_0072.jpg new file mode 100644 index 00000000..dfad82b0 Binary files /dev/null and b/modules/gallery/tests/images/DSC_0072.jpg differ diff --git a/modules/gallery/tests/images/P4050088.jpg b/modules/gallery/tests/images/P4050088.jpg new file mode 100644 index 00000000..62f4749d Binary files /dev/null and b/modules/gallery/tests/images/P4050088.jpg differ diff --git a/modules/gallery/tests/selenium/Add_Album.html b/modules/gallery/tests/selenium/Add_Album.html new file mode 100644 index 00000000..ccd4d0b7 --- /dev/null +++ b/modules/gallery/tests/selenium/Add_Album.html @@ -0,0 +1,52 @@ + + + + + + +AddAlbum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AddAlbum
    open/index.php/albums/1
    clicklink=Add album
    typenameseleniumtest
    typetitleSelenium Test Album
    typedescriptionTest
    click//button[@type='button']
    assertTextPresentSelenium Test Album
    + + diff --git a/modules/gallery/tests/selenium/Add_Comment.html b/modules/gallery/tests/selenium/Add_Comment.html new file mode 100644 index 00000000..b4b96ed2 --- /dev/null +++ b/modules/gallery/tests/selenium/Add_Comment.html @@ -0,0 +1,52 @@ + + + + + + +Add comment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Add comment
    open/index.php/albums/1
    clickAndWaitgPhotoId-2
    typegAuthorTest
    typegEmailtest@gmail.com
    typegTextThis is a selenium test comment.
    click//button[@type='submit']
    assertTextPresentThis is a selenium test comment.
    + + diff --git a/modules/gallery/tests/selenium/Add_Item.html b/modules/gallery/tests/selenium/Add_Item.html new file mode 100644 index 00000000..741dff65 --- /dev/null +++ b/modules/gallery/tests/selenium/Add_Item.html @@ -0,0 +1,62 @@ + + + + + + +AddItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AddItem
    open/index.php/albums/1
    clicklink=Add an item
    typenameseleniumitem.jpg
    typetitleSelenium Item
    typedescriptionTest item
    typefile/Users/ckieffer/Sites/gallery3.0/core/tests/images/DSC_0003.jpg
    click//button[@type='button']
    clicklink=X
    assertTextPresentSelenium Item
    + + diff --git a/modules/gallery/tests/selenium/Login.html b/modules/gallery/tests/selenium/Login.html new file mode 100644 index 00000000..5e17a3c7 --- /dev/null +++ b/modules/gallery/tests/selenium/Login.html @@ -0,0 +1,47 @@ + + + + + + +Login + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Login
    open/index.php/albums/1
    clickgLoginLink
    typegNameadmin
    typegPasswordadmin
    clickAndWait//button[@type='button']
    clickAndWaitgUserProfileLink
    + + diff --git a/modules/gallery/tests/test.jpg b/modules/gallery/tests/test.jpg new file mode 100644 index 00000000..1f3525e5 Binary files /dev/null and b/modules/gallery/tests/test.jpg differ diff --git a/modules/gallery/views/admin_advanced_settings.html.php b/modules/gallery/views/admin_advanced_settings.html.php new file mode 100644 index 00000000..1f3825bd --- /dev/null +++ b/modules/gallery/views/admin_advanced_settings.html.php @@ -0,0 +1,34 @@ + +
    +

    +

    + +

    +
      +
    • + ") ?> +
    • +
    + + + + + + + + + module_name == "core" && $var->name == "_cache") continue ?> + + + + + + +
    module_name ?> name ?> + module_name/$var->name") ?>" + class="gDialogLink" + title=" $var->name, "module_name" => $var->module_name)) ?>"> + value ?> + +
    +
    diff --git a/modules/gallery/views/admin_block_log_entries.html.php b/modules/gallery/views/admin_block_log_entries.html.php new file mode 100644 index 00000000..db6313e1 --- /dev/null +++ b/modules/gallery/views/admin_block_log_entries.html.php @@ -0,0 +1,11 @@ + + diff --git a/modules/gallery/views/admin_block_news.html.php b/modules/gallery/views/admin_block_news.html.php new file mode 100644 index 00000000..cb276ae5 --- /dev/null +++ b/modules/gallery/views/admin_block_news.html.php @@ -0,0 +1,11 @@ + +
      + +
    • + "> +

      + +

      +
    • + +
    diff --git a/modules/gallery/views/admin_block_photo_stream.html.php b/modules/gallery/views/admin_block_photo_stream.html.php new file mode 100644 index 00000000..e8a4d933 --- /dev/null +++ b/modules/gallery/views/admin_block_photo_stream.html.php @@ -0,0 +1,14 @@ + + +

    + +

    diff --git a/modules/gallery/views/admin_block_platform.html.php b/modules/gallery/views/admin_block_platform.html.php new file mode 100644 index 00000000..6b79f047 --- /dev/null +++ b/modules/gallery/views/admin_block_platform.html.php @@ -0,0 +1,18 @@ + +
      +
    • + PHP_OS)) ?> +
    • +
    • + function_exists("apache_get_version") ? apache_get_version() : t("Unknown"))) ?> +
    • +
    • + phpversion())) ?> +
    • +
    • + Database::instance()->query("SELECT version() as v")->current()->v)) ?> +
    • +
    • + $load_average)) ?> +
    • +
    diff --git a/modules/gallery/views/admin_block_stats.html.php b/modules/gallery/views/admin_block_stats.html.php new file mode 100644 index 00000000..2d975073 --- /dev/null +++ b/modules/gallery/views/admin_block_stats.html.php @@ -0,0 +1,12 @@ + +
      +
    • + module::get_var("core", "version"))) ?> +
    • +
    • + $album_count)) ?> +
    • +
    • + $photo_count)) ?> +
    • +
    diff --git a/modules/gallery/views/admin_block_welcome.html.php b/modules/gallery/views/admin_block_welcome.html.php new file mode 100644 index 00000000..488fa908 --- /dev/null +++ b/modules/gallery/views/admin_block_welcome.html.php @@ -0,0 +1,20 @@ + +

    + +

    +
      +
    • + graphics and language settings.", + array("graphics_url" => url::site("admin/graphics"), + "language_url" => url::site("admin/languages"))) ?> +
    • +
    • + choose a theme, or customize the way it looks.", + array("theme_url" => url::site("admin/theme"), + "theme_details_url" => url::site("admin/theme_details"))) ?> +
    • +
    • + install modules to add cool features!", + array("modules_url" => url::site("admin/modules"))) ?> +
    • +
    diff --git a/modules/gallery/views/admin_dashboard.html.php b/modules/gallery/views/admin_dashboard.html.php new file mode 100644 index 00000000..c266d7e1 --- /dev/null +++ b/modules/gallery/views/admin_dashboard.html.php @@ -0,0 +1,38 @@ + + +
    + +
    diff --git a/modules/gallery/views/admin_graphics.html.php b/modules/gallery/views/admin_graphics.html.php new file mode 100644 index 00000000..08374471 --- /dev/null +++ b/modules/gallery/views/admin_graphics.html.php @@ -0,0 +1,28 @@ + + +
    +

    +

    + +

    + +

    + + +
    +

    + +
    +
    + diff --git a/modules/gallery/views/admin_graphics_gd.html.php b/modules/gallery/views/admin_graphics_gd.html.php new file mode 100644 index 00000000..cae68b74 --- /dev/null +++ b/modules/gallery/views/admin_graphics_gd.html.php @@ -0,0 +1,29 @@ + +
    gd["GD Version"] ? " gInstalledToolkit" : " gUnavailable" ?>"> + " alt="" /> +

    +

    + GD website for more information.", + array("url" => "http://www.boutell.com/gd")) ?> +

    + gd["GD Version"] && function_exists('imagerotate')): ?> +

    + $tk->gd["GD Version"])) ?> +

    +

    + +

    + gd["GD Version"]): ?> +

    + $tk->gd["GD Version"])) ?> +

    +

    + +

    + +

    + +

    + +
    diff --git a/modules/gallery/views/admin_graphics_graphicsmagick.html.php b/modules/gallery/views/admin_graphics_graphicsmagick.html.php new file mode 100644 index 00000000..720a9459 --- /dev/null +++ b/modules/gallery/views/admin_graphics_graphicsmagick.html.php @@ -0,0 +1,21 @@ + +
    graphicsmagick ? " gInstalledToolkit" : " gUnavailable" ?>"> +

    + " alt="" /> +

    + GraphicsMagick website for more information.", + array("url" => "http://www.graphicsmagick.org")) ?> +

    + graphicsmagick): ?> +

    + $tk->graphicsmagick)) ?> +

    +

    + +

    + +

    + +

    + +
    diff --git a/modules/gallery/views/admin_graphics_imagemagick.html.php b/modules/gallery/views/admin_graphics_imagemagick.html.php new file mode 100644 index 00000000..c7468eed --- /dev/null +++ b/modules/gallery/views/admin_graphics_imagemagick.html.php @@ -0,0 +1,21 @@ + +
    imagemagick ? " gInstalledToolkit" : " gUnavailable" ?>"> +

    + " alt="" /> +

    + ImageMagick website for more information.", + array("url" => "http://www.imagemagick.org")) ?> +

    + imagemagick): ?> +

    + $tk->imagemagick)) ?> +

    +

    + +

    + +

    + +

    + +
    diff --git a/modules/gallery/views/admin_graphics_none.html.php b/modules/gallery/views/admin_graphics_none.html.php new file mode 100644 index 00000000..5306a70d --- /dev/null +++ b/modules/gallery/views/admin_graphics_none.html.php @@ -0,0 +1,7 @@ + +
    +

    +

    + +

    +
    diff --git a/modules/gallery/views/admin_languages.html.php b/modules/gallery/views/admin_languages.html.php new file mode 100644 index 00000000..2b43f1b4 --- /dev/null +++ b/modules/gallery/views/admin_languages.html.php @@ -0,0 +1,15 @@ + +
    +

    + + + +

    + " + class="gDialogLink"> + + + +

    + +
    diff --git a/modules/gallery/views/admin_maintenance.html.php b/modules/gallery/views/admin_maintenance.html.php new file mode 100644 index 00000000..bc060a7b --- /dev/null +++ b/modules/gallery/views/admin_maintenance.html.php @@ -0,0 +1,181 @@ + +
    +

    +

    + +

    + +
    +

    + + + + + + + + + + + + + +
    + + + + + +
    + name ?> + + description ?> + + callback?csrf=$csrf") ?>" + class="gDialogLink"> + + +
    +
    + + count()): ?> +
    +

    + " + class="gButtonLink ui-icon-left ui-state-default ui-corner-all right"> + + + + + + + + + + + + + "> + + + + + + + + +
    + + + + + + + + + + + +
    + updated) ?> + + name ?> + + done): ?> + state == "cancelled"): ?> + + + + state == "stalled"): ?> + + + $task->percent_complete)) ?> + + + status ?> + + owner()->name ?> + + state == "stalled"): ?> + id?csrf=$csrf") ?>"> + + + + id?csrf=$csrf") ?>"> + + +
    +
    + + + count()): ?> +
    + " + class="gButtonLink ui-icon-left ui-state-default ui-corner-all right"> + + +

    + + + + + + + + + + + "> + + + + + + + + +
    + + + + + + + + + + + +
    + updated) ?> + + name ?> + + state == "success"): ?> + + state == "error"): ?> + + state == "cancelled"): ?> + + + + status ?> + + owner()->name ?> + + done): ?> + id?csrf=$csrf") ?>"> + + + + id?csrf=$csrf") ?>"> + + + id?csrf=$csrf") ?>"> + + + +
    +
    + +
    diff --git a/modules/gallery/views/admin_maintenance_task.html.php b/modules/gallery/views/admin_maintenance_task.html.php new file mode 100644 index 00000000..1ee02311 --- /dev/null +++ b/modules/gallery/views/admin_maintenance_task.html.php @@ -0,0 +1,32 @@ + + +
    +
    +
    +
    + + +
    +
    diff --git a/modules/gallery/views/admin_modules.html.php b/modules/gallery/views/admin_modules.html.php new file mode 100644 index 00000000..3fddd6cd --- /dev/null +++ b/modules/gallery/views/admin_modules.html.php @@ -0,0 +1,32 @@ + +
    +

    +

    + +

    + +
    "> + + + + + + + + + + $module_info): ?> + "> + $module_name); ?> + locked) $data["disabled"] = 1; ?> + + + + + + + +
    name) ?> version ?> description) ?>
    + "/> +
    +
    diff --git a/modules/gallery/views/admin_theme_details.html.php b/modules/gallery/views/admin_theme_details.html.php new file mode 100644 index 00000000..eb450b16 --- /dev/null +++ b/modules/gallery/views/admin_theme_details.html.php @@ -0,0 +1,6 @@ + +
    +

    + + +
    diff --git a/modules/gallery/views/admin_themes.html.php b/modules/gallery/views/admin_themes.html.php new file mode 100644 index 00000000..f85bce70 --- /dev/null +++ b/modules/gallery/views/admin_themes.html.php @@ -0,0 +1,89 @@ + + + +

    +

    + +

    + +
    +

    +
    + " + alt="name ?>" /> +

    name ?>

    +

    + description ?> +

    +
    + +

    + +
    + +
    +

    +
    + " + alt="name ?>" /> +

    name ?>

    +

    + description ?> +

    +
    + +

    + +
    \ No newline at end of file diff --git a/modules/gallery/views/admin_themes_preview.html.php b/modules/gallery/views/admin_themes_preview.html.php new file mode 100644 index 00000000..a7aea172 --- /dev/null +++ b/modules/gallery/views/admin_themes_preview.html.php @@ -0,0 +1,7 @@ + +

    + "> + %theme_name", array("theme_name" => $info->name)) ?> + +

    + diff --git a/modules/gallery/views/after_install.html.php b/modules/gallery/views/after_install.html.php new file mode 100644 index 00000000..aa26858a --- /dev/null +++ b/modules/gallery/views/after_install.html.php @@ -0,0 +1,29 @@ + +

    + +

    + +

    + +

    + +

    + %user_name account. The very first thing you should do is to change your password to something that you'll remember.", array("user_name" => $user->name)) ?> +

    + +

    + id}") ?>" + title="" + id="gAfterInstallChangePasswordLink" class="gButtonLink ui-state-default ui-corners-all"> + +

    + +

    + Gallery website has news and information about Gallery Project and community.", array("url" => "http://gallery.menalto.com")) ?> +

    + +

    + documentation site or you can ask for help in the forums!", array("codex_url" => "http://codex.gallery2.org/Main_Page", "forum_url" => "http://gallery.menalto.com/forum")) ?> + diff --git a/modules/gallery/views/after_install_loader.html.php b/modules/gallery/views/after_install_loader.html.php new file mode 100644 index 00000000..baf91eed --- /dev/null +++ b/modules/gallery/views/after_install_loader.html.php @@ -0,0 +1,7 @@ + +" + href=""/> + diff --git a/modules/gallery/views/form.html.php b/modules/gallery/views/form.html.php new file mode 100644 index 00000000..ec2a56a9 --- /dev/null +++ b/modules/gallery/views/form.html.php @@ -0,0 +1,75 @@ + +"; +} +if ($title) { + print $title; +} + +if (!function_exists("DrawForm")) { + function DrawForm($inputs, $level=1) { + $error_messages = array(); + $prefix = str_repeat(" ", $level); + $haveGroup = false; + // On the first level, make sure we have a group if not add the

      tag now + if ($level == 1) { + foreach ($inputs as $input) { + $haveGroup |= $input->type == 'group'; + } + if (!$haveGroup) { + print "$prefix
        \n"; + } + } + + foreach ($inputs as $input) { + if ($input->type == 'group') { + print "$prefix
        \n"; + print "$prefix {$input->label}\n"; + print "$prefix
          \n"; + + DrawForm($input->inputs, $level + 2); + print "$prefix
        \n"; + + // Since hidden fields can only have name and value attributes lets just render it now + $hidden_prefix = "$prefix "; + foreach ($input->hidden as $hidden) { + print "$prefix {$hidden->render()}\n"; + } + print "$prefix
        \n"; + } else { + if ($input->error_messages()) { + print "$prefix
      • \n"; + } else { + print "$prefix
      • \n"; + } + + if ($input->label()) { + print "$prefix {$input->label()}\n"; + } + print "$prefix {$input->render()}\n"; + if ($input->message()) { + print "$prefix

        {$input->message()}

        \n"; + } + if ($input->error_messages()) { + foreach ($input->error_messages() as $error_message) { + print "$prefix

        \n"; + print "$prefix $error_message\n"; + print "$prefix

        \n"; + } + } + print "$prefix
      • \n"; + } + } + if ($level == 1 && !$haveGroup) { + print "$prefix
      \n"; + } + } +} +DrawForm($inputs); + +print($close); +?> diff --git a/modules/gallery/views/kohana_error_page.php b/modules/gallery/views/kohana_error_page.php new file mode 100644 index 00000000..d9bf9698 --- /dev/null +++ b/modules/gallery/views/kohana_error_page.php @@ -0,0 +1,118 @@ + + + + + + + <?= t("Something went wrong!") ?> + + + + admin ?> +
      +

      + +

      +

      + +

      + +

      + +

      + +
      + +
      +

      + +

      + + + + + + + diff --git a/modules/gallery/views/kohana_profiler.php b/modules/gallery/views/kohana_profiler.php new file mode 100644 index 00000000..c7534349 --- /dev/null +++ b/modules/gallery/views/kohana_profiler.php @@ -0,0 +1,35 @@ + + + +
      + + render(); ?> + +

      s

      +
      diff --git a/modules/gallery/views/l10n_client.html.php b/modules/gallery/views/l10n_client.html.php new file mode 100644 index 00000000..8f4092c7 --- /dev/null +++ b/modules/gallery/views/l10n_client.html.php @@ -0,0 +1,31 @@ + + diff --git a/modules/gallery/views/maintenance.html.php b/modules/gallery/views/maintenance.html.php new file mode 100644 index 00000000..f80b6e7a --- /dev/null +++ b/modules/gallery/views/maintenance.html.php @@ -0,0 +1,50 @@ + + + + + <?= t("Gallery - Maintenance Mode") ?> + + + + +

      + +

      +

      + +

      + + + + + diff --git a/modules/gallery/views/move_browse.html.php b/modules/gallery/views/move_browse.html.php new file mode 100644 index 00000000..4f69c0e9 --- /dev/null +++ b/modules/gallery/views/move_browse.html.php @@ -0,0 +1,47 @@ + + +

      + type == "photo"): ?> + + type == "movie"): ?> + + type == "album"): ?> + + +

      +
      +
        +
      • + +
      • +
      +
      id") ?>"> + + + " disabled="disabled"/> +
      +
      diff --git a/modules/gallery/views/move_tree.html.php b/modules/gallery/views/move_tree.html.php new file mode 100644 index 00000000..a3a4bc8f --- /dev/null +++ b/modules/gallery/views/move_tree.html.php @@ -0,0 +1,19 @@ + +thumb_tag(array(), 25); ?> +is_descendant($parent)): ?> + title ?> + + title ?> + + diff --git a/modules/gallery/views/permissions_browse.html.php b/modules/gallery/views/permissions_browse.html.php new file mode 100644 index 00000000..afd87c2b --- /dev/null +++ b/modules/gallery/views/permissions_browse.html.php @@ -0,0 +1,56 @@ + + +
      + +
        +
      • + AllowOverride FileInfo Options to fix this.", array("url" => "http://httpd.apache.org/docs/2.0/mod/core.html#allowoverride")) ?> +
      • +
      + + + +
      diff --git a/modules/gallery/views/permissions_form.html.php b/modules/gallery/views/permissions_form.html.php new file mode 100644 index 00000000..3dbd0d98 --- /dev/null +++ b/modules/gallery/views/permissions_form.html.php @@ -0,0 +1,94 @@ + +
      + +
      + + + + + + + + + + + + + + + name, $item) ?> + name, $item) ?> + name, $item) ?> + + + + + + + + + + + + + + + + + + + + + +
      name ?>
      display_name) ?> + <?= t('denied icon') ?> + + <?= t('locked icon') ?> + + + + <?= t('passive allowed icon') ?> + + + <?= t('inactive denied icon') ?> + + + + <?= t('inactive allowed icon') ?> + + + <?= t('passive denied icon') ?> + + + + <?= t('inactive allowed icon') ?> + + id == 1): ?> + <?= t('denied icon') ?> + + + <?= t('denied icon') ?> + + + + id == 1): ?> + <?= t('allowed icon') ?> + + + <?= t('allowed icon') ?> + + + + <?= t('inactive denied icon') ?> + +
      +
      +
      diff --git a/modules/gallery/views/quick_pane.html.php b/modules/gallery/views/quick_pane.html.php new file mode 100644 index 00000000..95de972b --- /dev/null +++ b/modules/gallery/views/quick_pane.html.php @@ -0,0 +1,108 @@ + +type == "photo"): ?> + +type == "movie"): ?> + +type == "album"): ?> + + +id?page_type=$page_type") ?>" + title=""> + + + + + +is_photo() && graphics::can("rotate")): ?> +id/ccw?csrf=$csrf&page_type=$page_type") ?>" + title=""> + + + + + +id/cw?csrf=$csrf&page_type=$page_type") ?>" + title=""> + + + + + + + + +type == "photo"): ?> + +type == "movie"): ?> + +type == "album"): ?> + + +id") ?>" + title=""> + + + + + + + +parent())): ?> +type == "photo"): ?> + +type == "movie"): ?> + +type == "album"): ?> +album_cover_item_id)): ?> +album_cover_item_id) ? " ui-state-disabled" : "" ?> + + + +id?csrf=$csrf&page_type=$page_type") ?>" + title=""> + + + + + +type == "photo"): ?> + + +type == "movie"): ?> + + +type == "album"): ?> + + + +id?csrf=$csrf&page_type=$page_type") ?>" ref="" id="gQuickDelete" title=""> + + + + + + +is_album()): ?> +"> + + + + + + + diff --git a/modules/gallery/views/scaffold.html.php b/modules/gallery/views/scaffold.html.php new file mode 100644 index 00000000..765464b5 --- /dev/null +++ b/modules/gallery/views/scaffold.html.php @@ -0,0 +1,169 @@ + + + + Gallery3 Scaffold + + + +
      +
      + "/> +
      +
      +

      Gallery3 Scaffold

      +

      + This is + a scaffold: + a temporary structure built to support the developers as + they create the real product. As we flesh out Gallery 3, + we'll make it possible for you to peer inside and see the + application taking shape. Eventually, this page will go + away and you'll start in the application itself. In the + meantime, here are some useful links to get you started. +

      + + 0): ?> +
      +

      + + ( albums, photos, comments, tags) +

      +
      + + +
      +
      + Generate Test Data +

      + add: [ + + + + ] photos and albums +

      +

      + add: [ + + + + ] albums only +

      +

      + add: [ + + + + ] comments +

      +

      + add: [ + + + + ] tags +

      +
      +
      + Packaging + ">Make Package +
      +
      +
      +
      + + diff --git a/modules/gallery/views/simple_uploader.html.php b/modules/gallery/views/simple_uploader.html.php new file mode 100644 index 00000000..b6725c31 --- /dev/null +++ b/modules/gallery/views/simple_uploader.html.php @@ -0,0 +1,249 @@ + + + + + +
      "> +
      + $item->title)) ?> +
      +
      + +
      + +
        +
      • + suhosin.session.encrypt setting from Suhosin. You must disable this setting to upload photos.", + array("encrypt_url" => "http://www.hardened-php.net/suhosin/configuration.html#suhosin.session.encrypt", + "suhosin_url" => "http://www.hardened-php.net/suhosin/")) ?> +
      • +
      + + +

      + +

      +
        + parents() as $parent): ?> +
      • title ?>
      • + +
      • title ?>
      • +
      + +

      +
      +
      +
      + +
      + + + + +
      + + + + -- cgit v1.2.3