diff options
author | Nathan Kinkade <nkinkade@nkinka.de> | 2010-02-10 20:57:53 +0000 |
---|---|---|
committer | Nathan Kinkade <nkinkade@nkinka.de> | 2010-02-10 20:57:53 +0000 |
commit | 10e36fcf1b5acf07c5cc128105af03fb09aac89e (patch) | |
tree | c5e815b0a4c540d0dc7bc5f90dd1eae3df31017e | |
parent | 052476ef44ca801766cbd6bdbfe42d5a0a362e52 (diff) | |
parent | 8ef08d20883d9b9aa0b7560ce3bf6da8a6632149 (diff) |
Merge branch 'master' of git://github.com/gallery/gallery3
59 files changed, 1051 insertions, 366 deletions
diff --git a/installer/install.sql b/installer/install.sql index 20b632fa..dea324eb 100644 --- a/installer/install.sql +++ b/installer/install.sql @@ -74,10 +74,10 @@ CREATE TABLE {comments} ( PRIMARY KEY (`id`) ) DEFAULT CHARSET=utf8; SET character_set_client = @saved_cs_client; -DROP TABLE IF EXISTS {failed_logins}; +DROP TABLE IF EXISTS {failed_auths}; SET @saved_cs_client = @@character_set_client; SET character_set_client = utf8; -CREATE TABLE {failed_logins} ( +CREATE TABLE {failed_auths} ( `id` int(9) NOT NULL auto_increment, `count` int(9) NOT NULL, `name` varchar(255) NOT NULL, @@ -239,8 +239,8 @@ CREATE TABLE {modules} ( UNIQUE KEY `name` (`name`) ) AUTO_INCREMENT=10 DEFAULT CHARSET=utf8; SET character_set_client = @saved_cs_client; -INSERT INTO {modules} VALUES (1,1,'gallery',25); -INSERT INTO {modules} VALUES (2,1,'user',2); +INSERT INTO {modules} VALUES (1,1,'gallery',28); +INSERT INTO {modules} VALUES (2,1,'user',3); INSERT INTO {modules} VALUES (3,1,'comment',2); INSERT INTO {modules} VALUES (4,1,'organize',1); INSERT INTO {modules} VALUES (5,1,'info',1); @@ -377,7 +377,7 @@ CREATE TABLE {vars} ( `value` text, PRIMARY KEY (`id`), UNIQUE KEY `module_name` (`module_name`,`name`) -) AUTO_INCREMENT=39 DEFAULT CHARSET=utf8; +) AUTO_INCREMENT=40 DEFAULT CHARSET=utf8; SET character_set_client = @saved_cs_client; INSERT INTO {vars} VALUES (NULL,'gallery','active_site_theme','wind'); INSERT INTO {vars} VALUES (NULL,'gallery','active_admin_theme','admin_wind'); @@ -391,6 +391,7 @@ INSERT INTO {vars} VALUES (NULL,'gallery','time_format','H:i:s'); INSERT INTO {vars} VALUES (NULL,'gallery','show_credits','1'); INSERT INTO {vars} VALUES (NULL,'gallery','credits','Powered by <a href=\"%url\">Gallery %version</a>'); INSERT INTO {vars} VALUES (NULL,'gallery','simultaneous_upload_limit','5'); +INSERT INTO {vars} VALUES (NULL,'gallery','admin_area_timeout','5400'); INSERT INTO {vars} VALUES (NULL,'gallery','blocks_dashboard_sidebar','a:4:{i:2;a:2:{i:0;s:7:\"gallery\";i:1;s:11:\"block_adder\";}i:3;a:2:{i:0;s:7:\"gallery\";i:1;s:5:\"stats\";}i:4;a:2:{i:0;s:7:\"gallery\";i:1;s:13:\"platform_info\";}i:5;a:2:{i:0;s:7:\"gallery\";i:1;s:12:\"project_news\";}}'); INSERT INTO {vars} VALUES (NULL,'gallery','date_time_format','Y-M-d H:i:s'); INSERT INTO {vars} VALUES (NULL,'gallery','date_format','Y-M-d'); diff --git a/lib/gallery.common.css b/lib/gallery.common.css index 87498b40..553eb496 100644 --- a/lib/gallery.common.css +++ b/lib/gallery.common.css @@ -304,13 +304,16 @@ tr.g-error td.g-error { } .g-success, -.g-allowed, -tr.g-success td.g-success { - background: #d9efc2 url('images/ico-success.png') no-repeat .4em 50%; +.g-allowed { + background: #d9efc2 url('images/ico-success.png') no-repeat .4em 50%; +} + +tr.g-success { + background-image: none; } tr.g-success td.g-success { - background-color: transparent; + background-image: url('images/ico-success.png'); } .g-warning, diff --git a/modules/g2_import/controllers/admin_g2_import.php b/modules/g2_import/controllers/admin_g2_import.php index 6dd155b9..f216091a 100644 --- a/modules/g2_import/controllers/admin_g2_import.php +++ b/modules/g2_import/controllers/admin_g2_import.php @@ -32,13 +32,14 @@ class Admin_g2_import_Controller extends Admin_Controller { $view = new Admin_View("admin.html"); $view->content = new View("admin_g2_import.html"); $view->content->form = $this->_get_import_form(); - $view->content->version = g2_import::version(); + $view->content->version = ''; if (g2_import::is_initialized()) { $view->content->g2_stats = $g2_stats; $view->content->g2_sizes = $g2_sizes; $view->content->thumb_size = module::get_var("gallery", "thumb_size"); $view->content->resize_size = module::get_var("gallery", "resize_size"); + $view->content->version = g2_import::version(); } g2_import::restore_error_reporting(); print $view; diff --git a/modules/g2_import/controllers/g2.php b/modules/g2_import/controllers/g2.php index 5fd4400c..1d97653a 100644 --- a/modules/g2_import/controllers/g2.php +++ b/modules/g2_import/controllers/g2.php @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ -class G2_Controller extends Admin_Controller { +class G2_Controller extends Controller { /** * Redirect Gallery 2 urls to their appropriate matching Gallery 3 url. * @@ -35,15 +35,25 @@ class G2_Controller extends Admin_Controller { $id = $input->get("g2_itemId"); if ($id) { - $where = array("g2_id", "=", $id); + // Requests by id are either core.DownloadItem or + // core.ShowItem requests. + // Later versions of Gallery 2 don't specify g2_view if + // it's the default (core.ShowItem). + // And in some cases (bbcode, embedding) people are using + // the id style URLs although URL rewriting is enabled. + $where = array(array("g2_id", "=", $id)); + $view = $input->get("g2_view"); + if ($view) { + $where[] = array("g2_url", "like", "%g2_view=$view%"); + } // else: Assuming that the first search hit is sufficiently good. } else if ($path) { - $where = array("g2_url", "=", $path); + $where = array(array("g2_url", "=", $path)); } else { throw new Kohana_404_Exception(); } $g2_map = ORM::factory("g2_map") - ->merge_where(array($where)) + ->merge_where($where) ->find(); if (!$g2_map->loaded()) { @@ -51,9 +61,10 @@ class G2_Controller extends Admin_Controller { } $item = ORM::factory("item", $g2_map->g3_id); - if (!$item->loaded() || !access::can("view", $item)) { + if (!$item->loaded()) { throw new Kohana_404_Exception(); } + access::required("view", $item); // Redirect the user to the new url @@ -64,6 +75,7 @@ class G2_Controller extends Admin_Controller { case "resize": url::redirect($item->resize_url(true)); + case "file": case "full": url::redirect($item->file_url(true)); diff --git a/modules/g2_import/helpers/g2_import.php b/modules/g2_import/helpers/g2_import.php index 0fcc0539..faf08291 100644 --- a/modules/g2_import/helpers/g2_import.php +++ b/modules/g2_import/helpers/g2_import.php @@ -304,7 +304,15 @@ class g2_import_Core { if ($user) { $message = t("Loaded existing user: '%name'.", array("name" => $user->name)); } else { - $user = identity::create_user($g2_user->getUsername(), $g2_user->getfullname(), ""); + $user = identity::create_user($g2_user->getUsername(), $g2_user->getfullname(), + // Note: The API expects a password in cleartext. + // Just use the hashed password as an unpredictable + // value here. The user will have to reset the password. + $g2_user->getHashedPassword(), $g2_user->getEmail()); + if (class_exists("User_Model") && $user instanceof User_Model) { + // This will work if G2's password is a PasswordHash password as well. + $user->hashed_password = $g2_user->getHashedPassword(); + } $message = t("Created user: '%name'.", array("name" => $user->name)); } @@ -357,45 +365,54 @@ class g2_import_Core { } if ($g2_album->getParentId() == null) { - return t("Skipping Gallery 2 root album"); - } - $parent_album = ORM::factory("item", self::map($g2_album->getParentId())); - - $album = ORM::factory("item"); - $album->type = "album"; - $album->parent_id = self::map($g2_album->getParentId()); - $album->name = $g2_album->getPathComponent(); - $album->title = self::_decode_html_special_chars($g2_album->getTitle()); - $album->description = self::_decode_html_special_chars(self::extract_description($g2_album)); - $album->owner_id = self::map($g2_album->getOwnerId()); - $album->view_count = g2(GalleryCoreApi::fetchItemViewCount($g2_album_id)); - $album->created = $g2_album->getCreationTimestamp(); - - $order_map = array( - "originationTimestamp" => "captured", - "creationTimestamp" => "created", - "description" => "description", - "modificationTimestamp" => "updated", - "orderWeight" => "weight", - "pathComponent" => "name", - "summary" => "description", - "title" => "title", - "viewCount" => "view_count"); - $direction_map = array( - ORDER_ASCENDING => "asc", - ORDER_DESCENDING => "desc"); - if (array_key_exists($g2_order = $g2_album->getOrderBy(), $order_map)) { - $album->sort_column = $order_map[$g2_order]; - $album->sort_order = $direction_map[$g2_album->getOrderDirection()]; - } - $album->save(); - - self::import_keywords_as_tags($g2_album->getKeywords(), $album); + $album = item::root(); + } else { + $parent_album = ORM::factory("item", self::map($g2_album->getParentId())); + + $album = ORM::factory("item"); + $album->type = "album"; + $album->parent_id = self::map($g2_album->getParentId()); + $album->name = $g2_album->getPathComponent(); + $album->title = self::_decode_html_special_chars($g2_album->getTitle()); + $album->title or $album->title = $album->name; + $album->description = self::_decode_html_special_chars(self::extract_description($g2_album)); + $album->owner_id = self::map($g2_album->getOwnerId()); + $album->view_count = g2(GalleryCoreApi::fetchItemViewCount($g2_album_id)); + $album->created = $g2_album->getCreationTimestamp(); + + $order_map = array( + "originationTimestamp" => "captured", + "creationTimestamp" => "created", + "description" => "description", + "modificationTimestamp" => "updated", + "orderWeight" => "weight", + "pathComponent" => "name", + "summary" => "description", + "title" => "title", + "viewCount" => "view_count"); + $direction_map = array( + ORDER_ASCENDING => "asc", + ORDER_DESCENDING => "desc"); + // Only consider G2's first sort order + $g2_order = explode("|", $g2_album->getOrderBy() . ""); + $g2_order = $g2_order[0]; + $g2_order_direction = explode("|", $g2_album->getOrderDirection() . ""); + $g2_order_direction = $g2_order_direction[0]; + if (array_key_exists($g2_order, $order_map)) { + $album->sort_column = $order_map[$g2_order]; + $album->sort_order = $direction_map[$g2_order_direction]; + } + $album->save(); + + self::import_keywords_as_tags($g2_album->getKeywords(), $album); + } self::set_map( $g2_album_id, $album->id, "album", self::g2_url(array("view" => "core.ShowItem", "itemId" => $g2_album->getId()))); + + self::_import_permissions($g2_album, $album); } /** @@ -488,6 +505,7 @@ class g2_import_Core { $item->set_data_file($g2_path); $item->name = $g2_item->getPathComponent(); $item->title = self::_decode_html_special_chars($g2_item->getTitle()); + $item->title or $item->title = $item->name; $item->description = self::_decode_html_special_chars(self::extract_description($g2_item)); $item->owner_id = self::map($g2_item->getOwnerId()); $item->save(); @@ -509,6 +527,7 @@ class g2_import_Core { $item->set_data_file($g2_path); $item->name = $g2_item->getPathComponent(); $item->title = self::_decode_html_special_chars($g2_item->getTitle()); + $item->title or $item->title = $item->name; $item->description = self::_decode_html_special_chars(self::extract_description($g2_item)); $item->owner_id = self::map($g2_item->getOwnerId()); } catch (Exception $e) { @@ -541,6 +560,10 @@ class g2_import_Core { $item->save(); self::set_map($g2_item_id, $item->id, "item", $g2_item_url); + + self::set_map($g2_item_id, $item->id, "file", + self::g2_url(array("view" => "core.DownloadItem", "itemId" => $g2_item_id))); + $derivatives = g2(GalleryCoreApi::fetchDerivativesByItemIds(array($g2_item_id))); if (!empty($derivatives[$g2_item_id])) { foreach ($derivatives[$g2_item_id] as $derivative) { @@ -586,6 +609,125 @@ class g2_import_Core { array("&", "\"", "<", ">"), $value); } + private static $_permission_map = array( + "core.view" => "view", + "core.viewSource" => "view_full", + "core.edit" => "edit", + "core.addDataItem" => "add", + "core.addAlbumItem" => "add"); + + /** + * Imports G2 permissions, mapping G2's permission model to G3's + * much simplified permissions. + * + * - Ignores user permissions, G3 only supports group permissions. + * - Ignores item permissions, G3 only supports album permissions. + * + * G2 permission -> G3 permission + * --------------------------------- + * core.view view + * core.viewSource view_full + * core.edit edit + * core.addDataItem add + * core.addAlbumItem add + * core.viewResizes <ignored> + * core.delete <ignored> + * comment.* <ignored> + */ + private static function _import_permissions($g2_album, $g3_album) { + // No need to do anything if this album has the same G2 ACL as its parent. + if ($g2_album->getParentId() != null && + g2(GalleryCoreApi::fetchAccessListId($g2_album->getId())) == + g2(GalleryCoreApi::fetchAccessListId($g2_album->getParentId()))) { + return; + } + + $granted_permissions = self::_map_permissions($g2_album->getId()); + + if ($g2_album->getParentId() == null) { + // Compare to current permissions, and change them if necessary. + $g3_parent_album = item::root(); + } else { + $g3_parent_album = $g3_album->parent(); + } + $granted_parent_permissions = array(); + $perm_ids = array_unique(array_values(self::$_permission_map)); + foreach (identity::groups() as $group) { + $granted_parent_permissions[$group->id] = array(); + foreach ($perm_ids as $perm_id) { + if (access::group_can($group, $perm_id, $g3_parent_album)) { + $granted_parent_permissions[$group->id][$perm_id] = 1; + } + } + } + + // Note: Only registering permissions if they're not the same as + // the inherited ones. + foreach ($granted_permissions as $group_id => $permissions) { + if (!isset($granted_parent_permissions[$group_id])) { + foreach (array_keys($permissions) as $perm_id) { + access::allow(identity::lookup_group($group_id), $perm_id, $g3_album); + } + } else if ($permissions != $granted_parent_permissions[$group_id]) { + $parent_permissions = $granted_parent_permissions[$group_id]; + // @todo Probably worth caching the group instances. + $group = identity::lookup_group($group_id); + // Note: Cannot use array_diff_key. + foreach (array_keys($permissions) as $perm_id) { + if (!isset($parent_permissions[$perm_id])) { + access::allow($group, $perm_id, $g3_album); + } + } + foreach (array_keys($parent_permissions) as $perm_id) { + if (!isset($permissions[$perm_id])) { + access::deny($group, $perm_id, $g3_album); + } + } + } + } + + foreach ($granted_parent_permissions as $group_id => $parent_permissions) { + if (isset($granted_permissions[$group_id])) { + continue; // handled above + } + $group = identity::lookup_group($group_id); + foreach (array_keys($parent_permissions) as $perm_id) { + access::deny($group, $perm_id, $g3_album); + } + } + } + + /** + * Loads all the granted group G2 permissions for a specific + * album and returns an array with G3 groups ids and G3 permission ids. + */ + private static function _map_permissions($g2_album_id) { + $g2_permissions = g2(GalleryCoreApi::fetchAllPermissionsForItem($g2_album_id)); + $permissions = array(); + foreach ($g2_permissions as $entry) { + // @todo Do something about user permissions? E.g. map G2's user albums + // to a user-specific group in G3? + if (!isset($entry["groupId"])) { + continue; + } + $g2_permission_id = $entry["permission"]; + if (!isset(self::$_permission_map[$g2_permission_id])) { + continue; + } + $group_id = self::map($entry["groupId"]); + if ($group_id == null) { + // E.g. the G2 admin group isn't mapped. + continue; + } + $permission_id = self::$_permission_map[$g2_permission_id]; + if (!isset($permissions[$group_id])) { + $permissions[$group_id] = array(); + } + $permissions[$group_id][$permission_id] = 1; + } + return $permissions; + } + /** * Import a single comment. */ @@ -599,6 +741,12 @@ class g2_import_Core { array("id" => $g2_comment_id, "exception" => (string)$e)); } + $item_id = self::map($g2_comment->getParentId()); + if (empty($item_id)) { + // Item was not mapped. + return; + } + $text = $g2_comment->getSubject(); if ($text) { $text .= " "; @@ -609,18 +757,17 @@ class g2_import_Core { // we don't trigger spam filtering events $comment = ORM::factory("comment"); $comment->author_id = self::map($g2_comment->getCommenterId()); - $comment->guest_name = $g2_comment->getAuthor(); - $comment->item_id = self::map($g2_comment->getParentId()); + $comment->guest_name = ""; + if ($comment->author_id == identity::guest()->id) { + $comment->guest_name = $g2_comment->getAuthor(); + $comment->guest_name or $comment->guest_name = (string) t("Anonymous coward"); + } + $comment->item_id = $item_id; $comment->text = self::_transform_bbcode($text); $comment->state = "published"; $comment->server_http_host = $g2_comment->getHost(); $comment->created = $g2_comment->getDate(); $comment->save(); - - self::map($g2_comment->getId(), $comment->id); - return t("Imported comment '%comment' for item with id: %id", - array("id" => $comment->item_id, - "comment" => text::limit_words(nl2br(html::purify($comment->text)), 50))); } /** diff --git a/modules/g2_import/helpers/g2_import_task.php b/modules/g2_import/helpers/g2_import_task.php index e0212b33..b1179e22 100644 --- a/modules/g2_import/helpers/g2_import_task.php +++ b/modules/g2_import/helpers/g2_import_task.php @@ -19,14 +19,19 @@ */ class g2_import_task_Core { static function available_tasks() { + $version = ''; g2_import::lower_error_reporting(); if (g2_import::is_configured()) { g2_import::init(); + // Guard from common case where the import has been + // completed and the original files have been removed. + if (class_exists("GalleryCoreApi")) { + $version = g2_import::version(); + } } - $version = g2_import::version(); g2_import::restore_error_reporting(); - if (class_exists("GalleryCoreApi")) { + if (g2_import::is_initialized()) { return array(Task_Definition::factory() ->callback("g2_import_task::import") ->name(t("Import from Gallery 2")) @@ -68,13 +73,8 @@ class g2_import_task_Core { } $task->set("done", $done); - $root_g2_id = g2(GalleryCoreApi::getDefaultAlbumId()); - $root = ORM::factory("g2_map")->where("g2_id", "=", $root_g2_id)->find(); - if (!$root->loaded()) { - $root->g2_id = $root_g2_id; - $root->g3_id = 1; - $root->save(); - } + // Ensure G2 ACLs are compacted to speed up import. + g2(GalleryCoreApi::compactAccessLists()); } $modes = array("groups", "users", "albums", "items", "comments", "tags", "highlights", "done"); @@ -123,7 +123,9 @@ class g2_import_task_Core { case "albums": if (empty($queue)) { - $task->set("queue", $queue = g2(GalleryCoreApi::fetchAlbumTree())); + $g2_root_id = g2(GalleryCoreApi::getDefaultAlbumId()); + $tree = g2(GalleryCoreApi::fetchAlbumTree()); + $task->set("queue", $queue = array($g2_root_id => $tree)); } $log_message = g2_import::import_album($queue); if ($log_message) { diff --git a/modules/g2_import/views/admin_g2_import.html.php b/modules/g2_import/views/admin_g2_import.html.php index 6a5214a3..3b7afec2 100644 --- a/modules/g2_import/views/admin_g2_import.html.php +++ b/modules/g2_import/views/admin_g2_import.html.php @@ -13,7 +13,7 @@ </p> <ul> <li> - <?= t("Permissions are <b>not imported</b>. You will have to set them again manually (for now).") ?> + <?= t("Please <b>review album permissions</b> after the import! Permissions are imported, but user specific and item specific permissions are not supported in Gallery 3 and thus ignored.") ?> </li> <li> <?= t("The only supported file formats are JPG, PNG and GIF, FLV and MP4. Other formats will be skipped.") ?> @@ -103,7 +103,7 @@ <IfModule mod_rewrite.c><br/> RewriteEngine On<br/> RewriteBase <?= html::clean(g2_import::$g2_base_url) ?><br/> - RewriteRule ^(.*)$ <?= url::site("g2/map?path=\$1") ?> [QSA,L]<br/> + RewriteRule ^(.*)$ <?= url::site("g2/map?path=\$1") ?> [QSA,L,R=301]<br/> </IfModule><br/> </code> </div> diff --git a/modules/gallery/controllers/admin.php b/modules/gallery/controllers/admin.php index e4216991..7706e9fc 100644 --- a/modules/gallery/controllers/admin.php +++ b/modules/gallery/controllers/admin.php @@ -21,7 +21,7 @@ class Admin_Controller extends Controller { private $theme; public function __construct($theme=null) { - if (!(identity::active_user()->admin)) { + if (!identity::active_user()->admin) { access::forbidden(); } @@ -29,6 +29,10 @@ class Admin_Controller extends Controller { } public function __call($controller_name, $args) { + if (auth::must_reauth_for_admin_area()) { + return self::_prompt_for_reauth($controller_name, $args); + } + if (request::method() == "post") { access::verify_csrf(); } @@ -49,5 +53,13 @@ class Admin_Controller extends Controller { call_user_func_array(array(new $controller_name, $method), $args); } + + private static function _prompt_for_reauth($controller_name, $args) { + if (request::method() == "get" && !request::is_ajax()) { + // Avoid anti-phishing protection by passing the url as session variable. + Session::instance()->set("continue_url", url::current(true)); + } + url::redirect("reauthenticate"); + } } diff --git a/modules/gallery/controllers/admin_maintenance.php b/modules/gallery/controllers/admin_maintenance.php index 8e4845a9..d90fe0ea 100644 --- a/modules/gallery/controllers/admin_maintenance.php +++ b/modules/gallery/controllers/admin_maintenance.php @@ -46,15 +46,6 @@ class Admin_Maintenance_Controller extends Admin_Controller { ->where("done", "=", 0)->order_by("updated", "DESC")->find_all(); $view->content->finished_tasks = ORM::factory("task") ->where("done", "=", 1)->order_by("updated", "DESC")->find_all(); - $task_buttons = - new ArrayObject(array((object)array("text" => t("run"), - "url" =>url::site("admin/maintenance/start")))); - module::event("admin_maintenance_task_buttons", $task_buttons); - $view->content->task_buttons = $task_buttons; - - $maintenance_content = new ArrayObject(); - module::event("admin_maintenance_content", $maintenance_content); - $view->content->task_maintenance_content = $maintenance_content; print $view; } diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php index a378f3ee..e1985cfb 100644 --- a/modules/gallery/controllers/albums.php +++ b/modules/gallery/controllers/albums.php @@ -28,20 +28,13 @@ class Albums_Controller extends Items_Controller { // sure that we're actually receiving an object Kohana::show_404(); } - $page_size = module::get_var("gallery", "page_size", 9); + if (!access::can("view", $album)) { - if ($album->id == 1) { - $view = new Theme_View("page.html", "other", "login"); - $view->page_title = t("Log in to Gallery"); - $view->content = new View("login_ajax.html"); - $view->content->form = auth::get_login_form("login/auth_html"); - print $view; - return; - } else { - access::forbidden(); - } + print auth::require_login(); + return; } + $page_size = module::get_var("gallery", "page_size", 9); $input = Input::instance(); $show = $input->get("show"); diff --git a/modules/gallery/controllers/login.php b/modules/gallery/controllers/login.php index 1426f0d8..093c15da 100644 --- a/modules/gallery/controllers/login.php +++ b/modules/gallery/controllers/login.php @@ -44,9 +44,10 @@ class Login_Controller extends Controller { public function auth_html() { access::verify_csrf(); + $continue_url = Session::instance()->get("continue_url", null); list ($valid, $form) = $this->_auth("login/auth_html"); if ($valid) { - url::redirect(item::root()->abs_url()); + url::redirect($continue_url ? $continue_url : item::root()->abs_url()); } else { $view = new Theme_View("page.html", "other", "login"); $view->page_title = t("Log in to Gallery"); @@ -65,7 +66,7 @@ class Login_Controller extends Controller { $form->login->inputs["name"]->add_error("invalid_login", 1); $name = $form->login->inputs["name"]->value; log::warning("user", t("Failed login for %name", array("name" => $name))); - module::event("user_login_failed", $name); + module::event("user_auth_failed", $name); $valid = false; } } diff --git a/modules/gallery/controllers/movies.php b/modules/gallery/controllers/movies.php index b51282b3..8041066e 100644 --- a/modules/gallery/controllers/movies.php +++ b/modules/gallery/controllers/movies.php @@ -24,7 +24,11 @@ class Movies_Controller extends Items_Controller { // sure that we're actually receiving an object Kohana::show_404(); } - access::required("view", $movie); + + if (!access::can("view", $movie)) { + print auth::require_login(); + return; + } $where = array(array("type", "!=", "album")); $position = $movie->parent()->get_position($movie, $where); diff --git a/modules/gallery/controllers/photos.php b/modules/gallery/controllers/photos.php index b5da3884..778e9ae7 100644 --- a/modules/gallery/controllers/photos.php +++ b/modules/gallery/controllers/photos.php @@ -24,7 +24,11 @@ class Photos_Controller extends Items_Controller { // sure that we're actually receiving an object Kohana::show_404(); } - access::required("view", $photo); + + if (!access::can("view", $photo)) { + print auth::require_login(); + return; + } $where = array(array("type", "!=", "album")); $position = $photo->parent()->get_position($photo, $where); diff --git a/modules/gallery/controllers/reauthenticate.php b/modules/gallery/controllers/reauthenticate.php new file mode 100644 index 00000000..dbd1cd21 --- /dev/null +++ b/modules/gallery/controllers/reauthenticate.php @@ -0,0 +1,73 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2009 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Reauthenticate_Controller extends Controller { + public function index($share_translations_form=null) { + if (!identity::active_user()->admin) { + access::forbidden(); + } + return self::_show_form(self::_form()); + } + + public function auth() { + if (!identity::active_user()->admin) { + access::forbidden(); + } + access::verify_csrf(); + + $form = self::_form(); + $valid = $form->validate(); + $user = identity::active_user(); + if ($valid) { + message::success(t("Successfully re-authenticated!")); + module::event("user_auth", $user); + $continue_url = Session::instance()->get_once("continue_url", "admin"); + url::redirect($continue_url); + } else { + $name = $user->name; + log::warning("user", t("Failed re-authentication for %name", array("name" => $name))); + module::event("user_auth_failed", $name); + return self::_show_form($form); + } + } + + private static function _show_form($form) { + $view = new Theme_View("page.html", "other", "reauthenticate"); + $view->page_title = t("Re-authenticate"); + $view->content = new View("reauthenticate.html"); + $view->content->form = $form; + $view->content->user_name = identity::active_user()->name; + print $view; + } + + private static function _form() { + $form = new Forge("reauthenticate/auth", "", "post", array("id" => "g-reauthenticate-form")); + $form->set_attr('class', "g-narrow"); + $group = $form->group("reauthenticate")->label(t("Re-authenticate")); + $group->password("password")->label(t("Password"))->id("g-password")->class(null) + ->callback("auth::validate_too_many_failed_auth_attempts") + ->callback("user::valid_password") + ->error_messages("invalid", t("Incorrect password")) + ->error_messages( + "too_many_failed_auth_attempts", + t("Too many incorrect passwords. Try again later")); + $group->submit("")->value(t("Submit")); + return $form; + } +} diff --git a/modules/gallery/css/upgrader.css b/modules/gallery/css/upgrader.css index 73da0ff4..2b3b5afd 100644 --- a/modules/gallery/css/upgrader.css +++ b/modules/gallery/css/upgrader.css @@ -26,6 +26,12 @@ div#footer { margin: 1em; } +table { + width: 600px; + margin-bottom: 10px; +} + +th.name, td.name { text-align: left; padding-left: 30px; @@ -53,11 +59,6 @@ tr.upgradeable td.gallery { color: #00d; } -table { - width: 600px; - margin-bottom: 10px; -} - p { font-size: .9em; } @@ -125,3 +126,31 @@ pre { margin: 0px; padding: 0px; } + +.rtl { + direction: rtl; +} + +.rtl th.name, +.rtl td.name { + text-align: right; + padding-right: 30px; +} + + +.rtl li:before { + content: ""; +} + +.rtl li:after { + content: "\00BB \0020"; +} + +.rtl ul { + margin-right: 0; + padding-right: 0; +} + +.rtl div#dialog a.close { + float: left; +} diff --git a/modules/gallery/helpers/auth.php b/modules/gallery/helpers/auth.php index 717cf40a..f5454f85 100644 --- a/modules/gallery/helpers/auth.php +++ b/modules/gallery/helpers/auth.php @@ -20,7 +20,7 @@ class auth_Core { static function get_login_form($url) { $form = new Forge($url, "", "post", array("id" => "g-login-form")); - $form->set_attr('class', "g-narrow"); + $form->set_attr("class", "g-narrow"); $group = $form->group("login")->label(t("Login")); $group->input("name")->label(t("Username"))->id("g-username")->class(null) ->callback("auth::validate_too_many_failed_logins") @@ -60,52 +60,87 @@ class auth_Core { } /** - * After there have been 5 failed login attempts, any failure leads to getting locked out for a + * After there have been 5 failed auth attempts, any failure leads to getting locked out for a * minute. */ - static function too_many_failed_logins($name) { - $failed_login = ORM::factory("failed_login") + static function too_many_failures($name) { + $failed = ORM::factory("failed_auth") ->where("name", "=", $name) ->find(); - return ($failed_login->loaded() && - $failed_login->count > 5 && - (time() - $failed_login->time < 60)); + return ($failed->loaded() && + $failed->count > 5 && + (time() - $failed->time < 60)); } static function validate_too_many_failed_logins($name_input) { - if (self::too_many_failed_logins($name_input->value)) { + if (self::too_many_failures($name_input->value)) { $name_input->add_error("too_many_failed_logins", 1); } } - static function validate_too_many_failed_password_changes($password_input) { - if (self::too_many_failed_logins(identity::active_user()->name)) { - $password_input->add_error("too_many_failed_password_changes", 1); + static function validate_too_many_failed_auth_attempts($form_input) { + if (self::too_many_failures(identity::active_user()->name)) { + $form_input->add_error("too_many_failed_auth_attempts", 1); } } /** - * Record a failed login for this user + * Record a failed authentication for this user */ - static function record_failed_auth_attempts($name) { - $failed_login = ORM::factory("failed_login") + static function record_failed_attempt($name) { + $failed = ORM::factory("failed_auth") ->where("name", "=", $name) ->find(); - if (!$failed_login->loaded()) { - $failed_login->name = $name; + if (!$failed->loaded()) { + $failed->name = $name; } - $failed_login->time = time(); - $failed_login->count++; - $failed_login->save(); + $failed->time = time(); + $failed->count++; + $failed->save(); } /** * Clear any failed logins for this user */ - static function clear_failed_logins($user) { - db::build() - ->delete("failed_logins") + static function clear_failed_attempts($user) { + ORM::factory("failed_auth") ->where("name", "=", $user->name) - ->execute(); + ->delete_all(); + } + + /** + * Checks whether the current user (= admin) must + * actively re-authenticate before access is given + * to the admin area. + */ + static function must_reauth_for_admin_area() { + if (!identity::active_user()->admin) { + access::forbidden(); + } + + $session = Session::instance(); + $last_active_auth = $session->get("active_auth_timestamp", 0); + $last_admin_area_activity = $session->get("admin_area_activity_timestamp", 0); + $admin_area_timeout = module::get_var("gallery", "admin_area_timeout"); + + if (max($last_active_auth, $last_admin_area_activity) + $admin_area_timeout < time()) { + return true; + } + + $session->set("admin_area_activity_timestamp", time()); + return false; + } + + /** + * Redirect to the login page. + */ + static function require_login() { + $view = new Theme_View("page.html", "other", "login"); + $view->page_title = t("Log in to Gallery"); + $view->content = new View("login_ajax.html"); + $view->content->form = auth::get_login_form("login/auth_html"); + // Avoid anti-phishing protection by passing the url as session variable. + Session::instance()->set("continue_url", url::current(true)); + return $view; } }
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery_block.php b/modules/gallery/helpers/gallery_block.php index be0f11b8..46742743 100644 --- a/modules/gallery/helpers/gallery_block.php +++ b/modules/gallery/helpers/gallery_block.php @@ -70,7 +70,7 @@ class gallery_block_Core { $block->css_id = "g-platform"; $block->title = t("Platform information"); $block->content = new View("admin_block_platform.html"); - if (is_readable("/proc/loadavg")) { + if (@is_readable("/proc/loadavg")) { $block->content->load_average = join(" ", array_slice(explode(" ", current(file("/proc/loadavg"))), 0, 3)); } else { diff --git a/modules/gallery/helpers/gallery_event.php b/modules/gallery/helpers/gallery_event.php index 7b538c49..63f33c12 100644 --- a/modules/gallery/helpers/gallery_event.php +++ b/modules/gallery/helpers/gallery_event.php @@ -110,19 +110,17 @@ class gallery_event_Core { graphics::choose_default_toolkit(); module::clear_var("gallery", "choose_default_tookit"); } - auth::clear_failed_auth_attempts($user); + Session::instance()->set("active_auth_timestamp", time()); + auth::clear_failed_attempts($user); } - static function user_login_failed($name) { - auth::record_failed_auth_attempts($name); + static function user_auth_failed($name) { + auth::record_failed_attempt($name); } - static function user_password_changed($user) { - auth::clear_failed_auth_attempts($user); - } - - static function user_password_change_failed($name) { - auth::record_failed_auth_attempts($name); + static function user_auth($user) { + auth::clear_failed_attempts($user); + Session::instance()->set("active_auth_timestamp", time()); } static function item_index_data($item, $data) { diff --git a/modules/gallery/helpers/gallery_installer.php b/modules/gallery/helpers/gallery_installer.php index 761843b0..dd53cf43 100644 --- a/modules/gallery/helpers/gallery_installer.php +++ b/modules/gallery/helpers/gallery_installer.php @@ -42,7 +42,7 @@ class gallery_installer { KEY (`tags`)) DEFAULT CHARSET=utf8;"); - $db->query("CREATE TABLE {failed_logins} ( + $db->query("CREATE TABLE {failed_auths} ( `id` int(9) NOT NULL auto_increment, `count` int(9) NOT NULL, `name` varchar(255) NOT NULL, @@ -287,7 +287,8 @@ class gallery_installer { // @todo this string needs to be picked up by l10n_scanner module::set_var("gallery", "credits", "Powered by <a href=\"%url\">Gallery %version</a>"); module::set_var("gallery", "simultaneous_upload_limit", 5); - module::set_version("gallery", 26); + module::set_var("gallery", "admin_area_timeout", 90 * 60); + module::set_version("gallery", 28); } static function upgrade($version) { @@ -526,6 +527,17 @@ class gallery_installer { ->execute(); module::set_version("gallery", $version = 26); } + + if ($version == 26) { + $db->query("RENAME TABLE {failed_logins} TO {failed_auths}"); + module::set_version("gallery", $version = 27); + } + + if ($version == 27) { + // Set the admin area timeout to 90 minutes + module::set_var("gallery", "admin_area_timeout", 90 * 60); + module::set_version("gallery", $version = 28); + } } static function uninstall() { @@ -534,7 +546,7 @@ class gallery_installer { $db->query("DROP TABLE IF EXISTS {access_intents}"); $db->query("DROP TABLE IF EXISTS {graphics_rules}"); $db->query("DROP TABLE IF EXISTS {incoming_translations}"); - $db->query("DROP TABLE IF EXISTS {failed_logins}"); + $db->query("DROP TABLE IF EXISTS {failed_auths}"); $db->query("DROP TABLE IF EXISTS {items}"); $db->query("DROP TABLE IF EXISTS {logs}"); $db->query("DROP TABLE IF EXISTS {modules}"); diff --git a/modules/gallery/helpers/gallery_theme.php b/modules/gallery/helpers/gallery_theme.php index 0018fd9a..9ffeb911 100644 --- a/modules/gallery/helpers/gallery_theme.php +++ b/modules/gallery/helpers/gallery_theme.php @@ -90,6 +90,17 @@ class gallery_theme_Core { $profiler = new Profiler(); $profiler->render(); } + + // Redirect to the root album when the admin session expires. + $redirect_url = url::abs_site(""); + $admin_area_timeout = 1000 * module::get_var("gallery", "admin_area_timeout"); + $admin_session_redirect_check = '<script type="text/javascript"> + var page_loaded_timestamp = new Date(); + setInterval("if (new Date() - page_loaded_timestamp > ' . $admin_area_timeout . + ') document.location = \'' . $redirect_url . '\';", 60 * 1000); + </script>'; + print $admin_session_redirect_check; + if ($session->get("l10n_mode", false)) { return L10n_Client_Controller::l10n_form(); } diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php index 41d49ce9..36193071 100644 --- a/modules/gallery/helpers/item.php +++ b/modules/gallery/helpers/item.php @@ -40,7 +40,56 @@ class item_Core { } $source->parent_id = $target->id; - $source->save(); + + // Moving may result in name or slug conflicts. If that happens, try up to 5 times to pick a + // random name (or slug) to avoid the conflict. + $orig_name = $source->name; + $orig_name_filename = pathinfo($source->name, PATHINFO_FILENAME); + $orig_name_extension = pathinfo($source->name, PATHINFO_EXTENSION); + $orig_slug = $source->slug; + for ($i = 0; $i < 5; $i++) { + try { + $source->save(); + if ($orig_name != $source->name) { + switch ($source->type) { + case "album": + message::info( + t("Album <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "photo": + message::info( + t("Photo <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "movie": + message::info( + t("Movie <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + } + } + break; + } catch (ORM_Validation_Exception $e) { + $rand = rand(10, 99); + $errors = $e->validation->errors(); + if (isset($errors["name"])) { + $source->name = $orig_name_filename . "-{$rand}." . $orig_name_extension; + unset($errors["name"]); + } + if (isset($errors["slug"])) { + $source->slug = $orig_slug . "-{$rand}"; + unset($errors["slug"]); + } + + if ($errors) { + // There were other validation issues-- we don't know how to handle those + throw $e; + } + } + } // If the target has no cover item, make this it. if ($target->album_cover_item_id == null) { diff --git a/modules/gallery/helpers/locales.php b/modules/gallery/helpers/locales.php index dc32b12f..e72d7ed9 100644 --- a/modules/gallery/helpers/locales.php +++ b/modules/gallery/helpers/locales.php @@ -63,50 +63,50 @@ class locales_Core { // @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["be_BY"] = "Беларускі"; // Belarusian - $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) + $l["af_ZA"] = "Afrikaans"; // Afrikaans + $l["ar_SA"] = "العربية"; // Arabic + $l["be_BY"] = "Беларускі"; // Belarusian + $l["bg_BG"] = "български"; // Bulgarian + $l["ca_ES"] = "Catalan"; // Catalan + $l["cs_CZ"] = "čeština"; // 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 do Brasil"; // Portuguese (BR) + $l["pt_PT"] = "Português ibérico"; // 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; @@ -131,9 +131,7 @@ class locales_Core { } static function is_rtl($locale=null) { - $locale or $locale = Gallery_I18n::instance()->locale(); - list ($language, $territory) = explode('_', $locale . "_"); - return in_array($language, array("he", "fa", "ar")); + return Gallery_I18n::instance()->is_rtl($locale); } /** diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php index 7033b7da..3c494e96 100644 --- a/modules/gallery/helpers/movie.php +++ b/modules/gallery/helpers/movie.php @@ -84,6 +84,20 @@ class movie_Core { " -an -ss 00:00:03 -an -r 1 -vframes 1" . " -y -f mjpeg " . escapeshellarg($output_file) . " 2>&1"; exec($cmd); + + clearstatcache(); // use $filename parameter when PHP_version is 5.3+ + if (filesize($output_file) == 0) { + // Maybe the movie is shorter, fall back to the first frame. + $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($input_file) . + " -an -an -r 1 -vframes 1" . + " -y -f mjpeg " . escapeshellarg($output_file) . " 2>&1"; + exec($cmd); + + clearstatcache(); + if (filesize($output_file) == 0) { + throw new Exception("@todo FFMPEG_FAILED"); + } + } } static function find_ffmpeg() { diff --git a/modules/gallery/libraries/Gallery_I18n.php b/modules/gallery/libraries/Gallery_I18n.php index 4e0c1f82..cfed046a 100644 --- a/modules/gallery/libraries/Gallery_I18n.php +++ b/modules/gallery/libraries/Gallery_I18n.php @@ -87,6 +87,16 @@ class Gallery_I18n_Core { return $this->_config['default_locale']; } + public function is_rtl($locale=null) { + $is_rtl = !empty($this->_config["force_rtl"]); + if (empty($is_rtl)) { + $locale or $locale = $this->locale(); + list ($language, $territory) = explode('_', $locale . "_"); + $is_rtl = in_array($language, array("he", "fa", "ar")); + } + return $is_rtl; + } + /** * Translates a localizable message. * diff --git a/modules/gallery/libraries/MY_Database.php b/modules/gallery/libraries/MY_Database.php index e2ef68cd..cb70104a 100644 --- a/modules/gallery/libraries/MY_Database.php +++ b/modules/gallery/libraries/MY_Database.php @@ -54,11 +54,18 @@ abstract class Database extends Database_Core { */ return $sql; } else if (strpos($sql, "CREATE TABLE") === 0) { - // Creating a new table add it to the table cache. + // 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"; + } else if (strpos($sql, "RENAME TABLE") === 0) { + // Renaming a table; add it to the table cache. + // You must use the form "TO {new_table_name}" exactly for this to work. + $open_brace = strpos($sql, "TO {") + 4; + $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)) { diff --git a/modules/gallery_unit_test/helpers/diff.php b/modules/gallery/libraries/MY_Input.php index 7b573732..dce569fd 100644 --- a/modules/gallery_unit_test/helpers/diff.php +++ b/modules/gallery/libraries/MY_Input.php @@ -17,10 +17,15 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ -class diff_Core { - static function compare($a, $b) { - fwrite(fopen($a_name = tempnam("/tmp", "test"), "w"), $a); - fwrite(fopen($b_name = tempnam("/tmp", "test"), "w"), $b); - return `diff $a_name $b_name`; +class Input extends Input_Core { + /** + * Modified form of Input::clean_input_keys() that replaces malformed values + * instead of dying on bad input. + * + * @param string string to clean + * @return string + */ + public function clean_input_keys($str) { + return preg_replace('#^[\pL0-9:_.-]++$#uD', '_', $str); } } diff --git a/modules/gallery/models/failed_login.php b/modules/gallery/models/failed_auth.php index 0b84c295..3c25f9d8 100644 --- a/modules/gallery/models/failed_login.php +++ b/modules/gallery/models/failed_auth.php @@ -17,4 +17,4 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ -class Failed_Login_Model extends ORM {} +class Failed_Auth_Model extends ORM {} diff --git a/modules/gallery/module.info b/modules/gallery/module.info index fd241066..ae300399 100644 --- a/modules/gallery/module.info +++ b/modules/gallery/module.info @@ -1,3 +1,3 @@ name = "Gallery 3" description = "Gallery core application" -version = 26 +version = 28 diff --git a/modules/gallery/tests/Database_Test.php b/modules/gallery/tests/Database_Test.php index 861f7bba..730785e2 100644 --- a/modules/gallery/tests/Database_Test.php +++ b/modules/gallery/tests/Database_Test.php @@ -130,6 +130,13 @@ class Database_Test extends Gallery_Unit_Test_Case { $this->assert_same($expected, $sql); } + function prefix_replacement_for_rename_table_test() { + $db = Database::instance("mock"); + $this->assert_same( + "RENAME TABLE g_test TO g_new_test", + $db->add_table_prefixes("RENAME TABLE {test} TO {new_test}")); + } + function prefix_no_replacement_test() { $sql = db::build("mock") ->from("test_tables") diff --git a/modules/gallery/tests/File_Structure_Test.php b/modules/gallery/tests/File_Structure_Test.php index 4590e95d..9b2b1480 100644 --- a/modules/gallery/tests/File_Structure_Test.php +++ b/modules/gallery/tests/File_Structure_Test.php @@ -195,7 +195,7 @@ class File_Structure_Test extends Gallery_Unit_Test_Case { foreach ($dir as $file) { $file_as_string = file_get_contents($file); if (preg_match('/\t/', $file_as_string)) { - foreach (split("\n", $file_as_string) as $l => $line) { + foreach (explode("\n", $file_as_string) as $l => $line) { if (preg_match('/\t/', $line)) { $errors[] = "$file:$l has tab(s) ($line)"; } diff --git a/modules/gallery/tests/Item_Helper_Test.php b/modules/gallery/tests/Item_Helper_Test.php index cdbdd324..50587702 100644 --- a/modules/gallery/tests/Item_Helper_Test.php +++ b/modules/gallery/tests/Item_Helper_Test.php @@ -19,6 +19,10 @@ */ class Item_Helper_Test extends Gallery_Unit_Test_Case { + public function setup() { + identity::set_active_user(identity::admin_user()); + } + public function viewable_test() { $album = test::random_album(); $item = test::random_photo($album); @@ -42,4 +46,65 @@ class Item_Helper_Test extends Gallery_Unit_Test_Case { $this->assert_equal("foo", item::convert_filename_to_slug("{[foo]}")); $this->assert_equal("foo-bar", item::convert_filename_to_slug("{[foo!@#!$@#^$@($!(@bar]}")); } + + public function move_test() { + $photo = test::random_photo(item::root()); + $dst_album = test::random_album(); + + item::move($photo, $dst_album); + $this->assert_same($dst_album->id, $photo->parent_id); + } + + + public function move_updates_album_covers_test() { + // 2 photos in the source album + $src_album = test::random_album(); + $photo1 = test::random_photo($src_album); + $photo2 = test::random_photo($src_album); + $src_album->reload(); + + // destination album + $dst_album = test::random_album(); + + item::move($photo1, $dst_album); + + // Refresh cached copies + $src_album->reload(); + $dst_album->reload(); + + // photo 2 becomes the album cover for the source album and photo 1 + // becomes the album cover for the destination + $this->assert_same($photo1->id, $dst_album->album_cover_item_id); + $this->assert_same($photo2->id, $src_album->album_cover_item_id); + } + + public function move_leaves_empty_album_with_no_album_cover_test() { + $src_album = test::random_album(); + $photo = test::random_photo($src_album); + + item::move($photo, item::root()); + + $src_album->reload(); + $this->assert_false($src_album->album_cover_item_id); + } + + public function move_conflicts_result_in_a_rename_test() { + $rand = rand(); + $photo1 = test::random_photo_unsaved(item::root()); + $photo1->name = "{$rand}.jpg"; + $photo1->slug = (string)$rand; + $photo1->save(); + + $src_album = test::random_album(); + $photo2 = test::random_photo_unsaved($src_album); + $photo2->name = "{$rand}.jpg"; + $photo2->slug = (string)$rand; + $photo2->save(); + + item::move($photo2, item::root()); + + $this->assert_same(item::root()->id, $photo2->parent_id); + $this->assert_not_same("{$rand}.jpg", $photo2->name); + $this->assert_not_same($rand, $photo2->slug); + } } diff --git a/modules/gallery/views/admin_maintenance.html.php b/modules/gallery/views/admin_maintenance.html.php index 19375670..ac597715 100644 --- a/modules/gallery/views/admin_maintenance.html.php +++ b/modules/gallery/views/admin_maintenance.html.php @@ -29,22 +29,16 @@ <?= $task->description ?> </td> <td> - <? foreach ($task_buttons as $button): ?> - <a href="<?= "{$button->url}/$task->callback?csrf=$csrf" ?>" + <a href="<?= url::site("admin/maintenance/start/$task->callback?csrf=$csrf") ?>" class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all"> - <?= html::clean($button->text) ?> + <?= t("run") ?> </a> - <? endforeach ?> </td> </tr> <? endforeach ?> </table> </div> - <? foreach ($task_maintenance_content as $content): ?> - <?= html::purify($content) ?> - <? endforeach ?> - <? if ($running_tasks->count()): ?> <div id="g-running-tasks"> <h2> <?= t("Running tasks") ?> </h2> diff --git a/modules/gallery/views/reauthenticate.html.php b/modules/gallery/views/reauthenticate.html.php new file mode 100644 index 00000000..8611d0f7 --- /dev/null +++ b/modules/gallery/views/reauthenticate.html.php @@ -0,0 +1,10 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div> + <p> + <?= t("The administration session has expired, please re-authenticate to access the administration area.") ?> + </p> + <p> + <?= t("You are currently logged in as %user_name.", array("user_name" => $user_name)) ?> + </p> + <?= $form ?> +</div>
\ No newline at end of file diff --git a/modules/gallery/views/upgrader.html.php b/modules/gallery/views/upgrader.html.php index 6cf0068d..55731440 100644 --- a/modules/gallery/views/upgrader.html.php +++ b/modules/gallery/views/upgrader.html.php @@ -6,7 +6,7 @@ media="screen,print,projection" /> <script src="<?= url::file("lib/jquery.js") ?>" type="text/javascript"></script> </head> - <body> + <body<? if (locales::is_rtl()) { echo ' class="rtl"'; } ?>> <div id="outer"> <img src="<?= url::file("modules/gallery/images/gallery.png") ?>" /> <div id="inner"> @@ -59,7 +59,7 @@ </p> <table> <tr class="<?= $done ? "muted" : "" ?>"> - <th> <?= t("Module name") ?> </th> + <th class="name"> <?= t("Module name") ?> </th> <th> <?= t("Installed version") ?> </th> <th> <?= t("Available version") ?> </th> </tr> @@ -112,7 +112,9 @@ <? else: // can_upgrade ?> <h1> <?= t("Who are you?") ?> </h1> <p> - <?= t("You're not logged in as an administrator, so we have to verify you to make sure it's ok for you to do an upgrade. To prove you can run an upgrade, create a file called <br/><b>%name</b> in your <b>gallery3/var/tmp</b> directory.", array("name" => "$upgrade_token")) ?> + <?= t("You're not logged in as an administrator, so we have to verify you to make sure it's ok for you to do an upgrade. To prove you can run an upgrade, create a file called <b>%name</b> in your <b>%tmp_dir_path</b> directory.", + array("name" => "$upgrade_token", + "tmp_dir_path" => "gallery3/var/tmp")) ?> </p> <a href="<?= url::site("upgrader?") ?>"><?= t("Ok, I've done that") ?></a> <? endif // can_upgrade ?> diff --git a/modules/gallery/views/user_profile.html.php b/modules/gallery/views/user_profile.html.php index 78e1c579..1c3e4ea2 100644 --- a/modules/gallery/views/user_profile.html.php +++ b/modules/gallery/views/user_profile.html.php @@ -63,6 +63,9 @@ <a class="g-button ui-icon-right ui-state-default ui-corner-all g-dialog-link" href="<?= url::site("users/form_change_password/{$user->id}") ?>"> <?= t("Change password") ?> </a> + <a class="g-button ui-icon-right ui-state-default ui-corner-all g-dialog-link" href="<?= url::site("users/form_change_email/{$user->id}") ?>"> + <?= t("Change email") ?> + </a> <? endif ?> <a id="g-profile-return" class="g-button ui-icon-right ui-state-default ui-corner-all" href="#"> diff --git a/modules/gallery/views/welcome_message.html.php b/modules/gallery/views/welcome_message.html.php index 24d01bab..caeeff66 100644 --- a/modules/gallery/views/welcome_message.html.php +++ b/modules/gallery/views/welcome_message.html.php @@ -15,15 +15,12 @@ </p> <p> - <a href="<?= url::site("form/edit/users/{$user->id}") ?>" + <a href="<?= url::site("user_profile/show/{$user->id}") ?>" title="<?= t("Edit your profile")->for_html_attr() ?>" id="g-after-install-change-password-link" class="g-button ui-state-default ui-corners-all"> - <?= t("Change password now") ?> + <?= t("Change password and email now") ?> </a> - <script type="text/javascript"> - $("#g-after-install-change-password-link").gallery_dialog(); - </script> </p> <p> diff --git a/modules/gallery_unit_test/helpers/test.php b/modules/gallery_unit_test/helpers/test.php index 3e116808..91bd1be5 100644 --- a/modules/gallery_unit_test/helpers/test.php +++ b/modules/gallery_unit_test/helpers/test.php @@ -82,4 +82,10 @@ class test_Core { // Reload so that ORM coerces all fields into strings. return $tag->save()->reload(); } + + static function diff($a, $b) { + fwrite(fopen($a_name = tempnam("/tmp", "test"), "w"), $a); + fwrite(fopen($b_name = tempnam("/tmp", "test"), "w"), $b); + return `diff $a_name $b_name`; + } } diff --git a/modules/gallery_unit_test/libraries/Gallery_Unit_Test_Case.php b/modules/gallery_unit_test/libraries/Gallery_Unit_Test_Case.php index 509b4125..545af0fe 100644 --- a/modules/gallery_unit_test/libraries/Gallery_Unit_Test_Case.php +++ b/modules/gallery_unit_test/libraries/Gallery_Unit_Test_Case.php @@ -24,7 +24,7 @@ class Gallery_Unit_Test_Case extends Unit_Test_Case { sprintf("Expected (%s) %s but received (%s) %s\n Diff: %s", gettype($expected), var_export($expected, true), gettype($actual), var_export($actual, true), - diff::compare(var_export($expected, true), var_export($actual, true))), + test::diff(var_export($expected, true), var_export($actual, true))), $debug); } return $this; diff --git a/modules/image_block/helpers/image_block_block.php b/modules/image_block/helpers/image_block_block.php index 185df850..82c92f19 100644 --- a/modules/image_block/helpers/image_block_block.php +++ b/modules/image_block/helpers/image_block_block.php @@ -26,7 +26,13 @@ class image_block_block_Core { $block = ""; switch ($block_id) { case "random_image": - $item = item::random_query(array(array("type", "!=", "album")))->find_all(1)->current(); + // The random_query approach is flawed and doesn't always return a + // result when there actually is one. Retry a *few* times. + // @todo Consider another fallback if further optimizations are necessary. + $attempts = 0; + do { + $item = item::random_query(array(array("type", "!=", "album")))->find_all(1)->current(); + } while (!$item && $attempts++ < 3); if ($item && $item->loaded()) { $block = new Block(); $block->css_id = "g-image-block"; diff --git a/modules/notification/helpers/notification.php b/modules/notification/helpers/notification.php index dfeab9fc..3e4854d7 100644 --- a/modules/notification/helpers/notification.php +++ b/modules/notification/helpers/notification.php @@ -107,8 +107,6 @@ class notification { t("Photo \"%title\" updated", array("title" => $original->title)) : t("Movie \"%title\" updated", array("title" => $original->title))); - Kohana_Log::add("error",print_r($v->render(),1)); - self::_notify($subscribers, $item, $v->render(), $v->subject); } diff --git a/modules/organize/controllers/organize.php b/modules/organize/controllers/organize.php index 4a4b9f13..9d9da65e 100644 --- a/modules/organize/controllers/organize.php +++ b/modules/organize/controllers/organize.php @@ -48,8 +48,12 @@ class Organize_Controller extends Controller { access::required("view", $target_album); access::required("add", $target_album); + $source_album = null; foreach (Input::instance()->post("source_ids") as $source_id) { $source = ORM::factory("item", $source_id); + if (empty($source_album)) { // get the source_album + $source_album = $source->parent(); + } if (!$source->contains($target_album)) { access::required("edit", $source); item::move($source, $target_album); @@ -57,8 +61,8 @@ class Organize_Controller extends Controller { } print json_encode( - array("tree" => (string)self::_expanded_tree(ORM::factory("item", 1), $target_album), - "grid" => (string)self::_get_micro_thumb_grid($target_album, 0))); + array("tree" => (string)self::_expanded_tree(ORM::factory("item", 1), $source_album), + "grid" => (string)self::_get_micro_thumb_grid($source_album, 0))); } function rearrange($target_id, $before_or_after) { @@ -69,6 +73,10 @@ class Organize_Controller extends Controller { access::required("view", $album); access::required("edit", $album); + //if (locales::is_rtl()) { // invert the position if the locale is rtl + // $before_or_after = $before_or_after == "after" ? "before" : "after"; + //} + $source_ids = Input::instance()->post("source_ids", array()); if ($album->sort_column != "weight") { diff --git a/modules/organize/css/organize.css b/modules/organize/css/organize.css index d8923ea7..7a8c3a5f 100644 --- a/modules/organize/css/organize.css +++ b/modules/organize/css/organize.css @@ -3,18 +3,18 @@ */ #g-organize { - height: auto; margin: 0 !important; + min-height: auto; padding: 0 !important; position: relative; width: 100%; } #g-organize-content-pane { - height: auto; + height: 100%; margin: 0 !important; padding: 0 !important; - position: relative; + position: absolute; width: 100%; } @@ -23,11 +23,15 @@ */ #g-organize #g-organize-tree-container { - height: 100%; - overflow: auto; margin: 0; + min-height: 100%; padding: 0; - width: 19%; + position: relative; + width: 20%; +} + +#g-organize #g-organize-tree-container h3 { + margin-bottom: 0.1em; } #g-organize-album-tree { @@ -36,6 +40,12 @@ #g-organize-album-tree ul li { padding: 0 0 .2em 1.2em; + width: 90%; +} + +.rtl #g-organize-album-tree ul li { + padding: 0 1.2em .2em 0; + width: 90%; } .g-organize-album span { @@ -49,15 +59,27 @@ width: auto; } +.rtl .g-organize-album-text { + cursor: pointer; + display: block; + margin: 2px 2px 1px 1px; + width: auto; +} + +.g-organize-album-text:hover { + border-width: 1px; + border-style: dotted; +} + /******************************************************************* * Album panel styles */ #g-organize #g-organize-detail { - height: 100%; margin: 0 !important; - overflow: hidden; + min-height: 100%; padding: 0 !important; + position: relative; width: 80%; } @@ -68,35 +90,27 @@ #g-organize #g-organize-detail .g-message-block li { padding-bottom: .2em; padding-top: .2em; - width: inherit; -} - -#g-organize-microthumb-panel { - height: 100%; - margin: 0 !important; - position: relative; - padding: 0 !important; - width: 100%; + width: auto; } #g-organize-microthumb-grid { - height: 100%; - overflow: auto; + border-width: 1px; + border-style: solid; + bottom: 1.8em; + left: 0; + margin: 0 !important; + overflow-x: hidden; + overflow-y: auto; padding: .4em !important; - position: relative; + position: absolute; + right: 0; + top: 1.6em; } - .g-organize-microthumb-grid-cell { - margin: 6px; - padding: 0 !important; - position: relative; -} - -.g-organize-microthumb { display: block; height: 100px; - margin: 0; - padding: .4em 0; + margin: 6px; + padding: .4em 0 !important; position: relative; text-align: center; width: 110px; @@ -106,7 +120,7 @@ z-index: 2000 !important; } -.g-organize-microthumb .ui-icon { +.g-organize-microthumb-grid-cell .ui-icon { bottom: 0; left: 0; position: absolute; @@ -118,11 +132,22 @@ */ #g-organize-controls { + bottom: 0; + height: 1.9em; + left: 0; margin: 0 !important; - padding: .2em .4em; + padding: .1em .4em; + position: absolute; + right: 0; } +#g-organize-controls #g-organize-sort-order-text { + padding: .2em 0 0 0; +} + + #g-organize-controls select { + margin-left: .42em; display: inline; } diff --git a/modules/organize/css/organize_theme.css b/modules/organize/css/organize_theme.css new file mode 100644 index 00000000..3d289755 --- /dev/null +++ b/modules/organize/css/organize_theme.css @@ -0,0 +1,16 @@ +/** ******************************************************************* + * Organize styles that are theme overrideable + *********************************************************************/ +.g-organize-microthumb-grid-cell.ui-selected { + background: #DFEFFC !important; +} + +#g-organize-microthumb-grid, +#g-organize-drop-target-marker, +.g-organize-album-text:hover { + border-color: #79B7E7; +} + +#g-organize-drop-target-marker { + background-color: #79B7E7; +} diff --git a/modules/organize/helpers/organize_theme.php b/modules/organize/helpers/organize_theme.php index 61b6fe7d..c2914675 100644 --- a/modules/organize/helpers/organize_theme.php +++ b/modules/organize/helpers/organize_theme.php @@ -27,6 +27,7 @@ class organize_theme { // approach that lets us continue to use the Kohana cascading filesystem. $theme->script("organize.js"); $theme->css("organize.css"); + $theme->css("organize_theme.css"); } } } diff --git a/modules/organize/js/organize.js b/modules/organize/js/organize.js index 76eadf85..5b90f402 100644 --- a/modules/organize/js/organize.js +++ b/modules/organize/js/organize.js @@ -4,50 +4,47 @@ handle: ".ui-selected", distance: 10, cursorAt: { left: -10, top: -10}, - appendTo: "#g-organize-microthumb-panel", + appendTo: "#g-organize-content-pane", helper: function(event, ui) { var selected = $(".ui-draggable.ui-selected img"); - if (selected.length) { - var set = $('<div class="g-drag-helper"></div>') - .css({ + var set = $('<div class="g-drag-helper"></div>') + .css({ zIndex: 2000, width: 80, height: Math.ceil(selected.length / 5) * 16 - }); - var offset = $(this).offset(); - var click = {left: event.pageX - offset.left, top: event.pageY - offset.top}; - - selected.each(function(i) { - var row = parseInt(i / 5); - var j = i - (row * 5); - var o = $(this).offset(); - var copy = $(this).clone() - .css({ - width: $(this).width(), height: $(this).height(), display: "block", - margin: 0, position: 'absolute', outline: '5px solid #fff', - left: o.left - event.pageX, top: o.top - event.pageY - }) - .appendTo(set) - .animate({ width: 10, height: 10, outlineWidth: 1, margin: 1, - left: (20 * j), top: (row * 20) }, 500); - }); - return set; - } - return null; + }); + + selected.each(function(i) { + var row = parseInt(i / 5); + var j = i - (row * 5); + var o = $(this).offset(); + var copy = $(this).clone() + .css({ + width: $(this).width(), height: $(this).height(), display: "block", + margin: 0, position: 'absolute', outline: '5px solid #fff', + left: o.left - event.pageX, top: o.top - event.pageY + }) + .appendTo(set) + .animate({ width: 10, height: 10, outlineWidth: 1, margin: 1, + left: (20 * j), top: (row * 20) }, 500); + }); + return set; }, start: function(event, ui) { - $("#g-organize-microthumb-panel .ui-selected").hide(); + $("#g-organize-microthumb-grid .ui-selected").hide(); }, drag: function(event, ui) { - var top = $("#g-organize-microthumb-panel").offset().top; - var height = $("#g-organize-microthumb-panel").height(); + var top = $("#g-organize-microthumb-grid").offset().top; + var height = $("#g-organize-microthumb-grid").height(); + var scrollTop = $("#g-organize-microthumb-grid").scrollTop(); if (ui.offset.top > height + top - 20) { - $("#g-organize-microthumb-panel").get(0).scrollTop += 100; + scrollTop += 100; } else if (ui.offset.top < top + 20) { - $("#g-organize-microthumb-panel").get(0).scrollTop = Math.max(0, $("#g-organize-microthumb-panel").get(0).scrollTop - 100); + scrollTop = Math.max(0, scrollTop - 100); } + $("#g-organize-microthumb-grid").scrollTop(scrollTop); } }, @@ -56,19 +53,18 @@ tolerance: "pointer", greedy: true, drop: function(event, ui) { - var before_or_after = null; - var target_id = null; - if ($(".currentDropTarget").length) { - before_or_after = $(".currentDropTarget").css("borderLeftStyle") == "solid" ? "before" : "after"; - target_id = $(".currentDropTarget").attr("ref"); - } else { - before_or_after = "after"; - target_id = $("#g-organize-microthumb-grid li:last").attr("ref"); + $(".g-mouse-drag-over").removeClass("g-mouse-drag-over"); + var target = $("#g-organize-drop-target-marker").data("drop_position"); + if (target == null) { + target = { + position: false, + id: $(".g-organize-microthumb-grid-cell:visible:last").attr("ref") + }; } $.organize.do_drop({ url: rearrange_url - .replace("__TARGET_ID__", target_id) - .replace("__BEFORE__", before_or_after), + .replace("__TARGET_ID__", target.id) + .replace("__BEFORE__", target.position ? "before" : "after"), source: $(ui.helper).children("img") }); } @@ -80,8 +76,8 @@ greedy: true, drop: function(event, ui) { if ($(event.target).hasClass("g-view-only")) { + $("#g-organize-drop-target-marker").remove(); $(".ui-selected").show(); - $(".g-organize-microthumb-grid-cell").css("borderStyle", "none"); } else { $.organize.do_drop({ url: move_url.replace("__ALBUM_ID__", $(event.target).attr("ref")), @@ -92,19 +88,31 @@ }, do_drop: function(options) { - $("#g-organize-microthumb-panel").selectable("destroy"); + $("#g-organize-microthumb-grid").selectable("destroy"); var source_ids = []; $(options.source).each(function(i) { source_ids.push($(this).attr("ref")); }); if (source_ids.length) { + var loading = $('<div class="g-dialog-loading-large"> </div>') + .css({bottom: 5, + opacity: .5, + left: 0, + position: "absolute", + right: 0, + top: 0, + zIndex: 2000 + }); + $("#g-organize-microthumb-grid").append(loading); + $.post(options.url, - { "source_ids[]": source_ids }, - function(data) { - $.organize._refresh(data); - }, - "json"); + { "source_ids[]": source_ids }, + function(data) { + $.organize._refresh(data); + $(".g-dialog-loading-large").remove(); + }, + "json"); } }, @@ -120,15 +128,50 @@ $.organize.set_handlers(); }, - mouse_move_handler: function(event) { + grid_mouse_leave_handler: function(event) { + if ($(".g-drag-helper").length && $("#g-organize-drop-target-marker").length) { + $("#g-organize-drop-target-marker").remove(); + } + }, + + grid_mouse_move_handler: function(event) { if ($(".g-drag-helper").length) { - $(".g-organize-microthumb-grid-cell").css({borderStyle: "hidden", margin: "6px"}); - $(".currentDropTarget").removeClass("currentDropTarget"); - var borderStyle = event.pageX < $(this).offset().left + $(this).width() / 2 ? - {borderLeftStyle: "solid", marginLeft: "2px"} : {borderRightStyle: "solid", marginRight: "2px"}; - $(this).addClass("currentDropTarget") - .css(borderStyle); + var organizeData = $("#g-organize").data("organizeData"); + var thumbGrid = $("#g-organize-microthumb-grid"); + var visibleCells = $(".g-organize-microthumb-grid-cell:visible"); + var scrollTop = thumbGrid.scrollTop(); + + var item = $(".g-mouse-drag-over"); + if (item.length == 0) { + var itemColumn = Math.floor((event.pageX - thumbGrid.offset().left) / organizeData.width); + itemColumn = organizeData.rtl ? organizeData.width - itemColumn : itemColumn; + var itemRow = Math.floor((event.pageY + scrollTop - thumbGrid.offset().top) / organizeData.height); + var itemIndex = Math.min(itemRow * organizeData.columns + itemColumn, visibleCells.length - 1); + item = visibleCells.get(itemIndex); + } + + var before = event.pageX < ($(item).offset().left + $(item).width() / 2); + var left = $(item).position().left + (before ? 0 : organizeData.width) - 3; + var top = $(item).position().top + 6 + scrollTop; + + if ($("#g-organize-drop-target-marker").length) { + $("#g-organize-drop-target-marker").remove(); + } + + var set = $('<div id="g-organize-drop-target-marker"></div>') + .css({zIndex: 2000, + width: 2, + height: 112, + borderWidth: 1, + borderStyle: "solid", + position: "absolute", + top: top, left: left + }) + .data("drop_position", {id: $(item).attr("ref"), + position: organizeData.rtl ? !before : before}); + thumbGrid.append(set); } + return true; }, /** @@ -140,9 +183,17 @@ $(".sf-menu li.sfHover ul").css("z-index", 68); $("#g-dialog").dialog("option", "zIndex", 70); $("#g-dialog").bind("dialogopen", function(event, ui) { - $("#g-organize").height($("#g-dialog").innerHeight() - 20); - $("#g-organize-microthumb-grid").height($("#g-dialog").innerHeight() - 91); - $("#g-organize-tree-container").height($("#g-dialog").innerHeight() - 60); + var outerHeight = $(".g-organize-microthumb-grid-cell").outerHeight(true); + var outerWidth = $(".g-organize-microthumb-grid-cell").outerWidth(true); + var gridInnerWidth = $("#g-organize-microthumb-grid").innerWidth() - 2 * parseFloat($("#g-organize-microthumb-grid").css("paddingLeft")); + $("#g-organize") + .height($("#g-dialog").innerHeight() - 20) + .data("organizeData", { + rtl: $("body").hasClass("rtl"), + height: outerHeight, + width: outerWidth, + columns: Math.floor(gridInnerWidth / outerWidth) + }); }); $("#g-dialog").bind("dialogclose", function(event, ui) { @@ -164,11 +215,24 @@ set_handlers: function() { $("#g-organize-microthumb-grid") .selectable({filter: ".g-organize-microthumb-grid-cell"}) + .mousemove($.organize.grid_mouse_move_handler) + .mouseleave($.organize.grid_mouse_leave_handler) .droppable($.organize.content_droppable); $(".g-organize-microthumb-grid-cell") - .draggable($.organize.micro_thumb_draggable) - .mouseleave($.organize.mouse_leave_handler) - .mousemove($.organize.mouse_move_handler); + // need to manually add this class in case we care calling with additional elements + .addClass("ui-selectee") + .mouseleave(function(event) { + if ($(".g-drag-helper").length) { + $(this).removeClass("g-mouse-drag-over"); + } + }) + .mouseenter(function(event) { + $(".g-mouse-drag-over").removeClass("g-mouse-drag-over"); + if ($(".g-drag-helper").length) { + $(this).addClass("g-mouse-drag-over"); + } + }) + .draggable($.organize.micro_thumb_draggable); $(".g-organize-album").droppable($.organize.branch_droppable); $(".g-organize-album-text").click($.organize.show_album); $("#g-organize-album-tree .ui-icon-plus,#g-organize-album-tree .ui-icon-minus").click($.organize.toggle_branch); @@ -210,18 +274,18 @@ if ($(parent).hasClass("g-view-only")) { return; } - $("#g-organize-microthumb-panel").selectable("destroy"); + $("#g-organize-microthumb-grid").selectable("destroy"); var id = $(event.currentTarget).attr("ref"); $(".g-organize-album-text.ui-state-focus").removeClass("ui-state-focus"); $(".g-organize-album-text[ref=" + id + "]").addClass("ui-state-focus"); - var url = $("#g-organize-microthumb-panel").attr("ref").replace("__ITEM_ID__", id).replace("__OFFSET__", 0); + var url = $("#g-organize-microthumb-grid").attr("ref").replace("__ITEM_ID__", id).replace("__OFFSET__", 0); $.get(url, {}, - function(data) { - $("#g-organize-microthumb-grid").html(data.grid); - $("#g-organize-sort-column").attr("value", data.sort_column); - $("#g-organize-sort-order").attr("value", data.sort_order); - $.organize.set_handlers(); - }, + function(data) { + $("#g-organize-microthumb-grid").html(data.grid); + $("#g-organize-sort-column").attr("value", data.sort_column); + $("#g-organize-sort-order").attr("value", data.sort_order); + $.organize.set_handlers(); + }, "json"); }, diff --git a/modules/organize/views/organize_dialog.html.php b/modules/organize/views/organize_dialog.html.php index 435f5ae3..38d05b81 100644 --- a/modules/organize/views/organize_dialog.html.php +++ b/modules/organize/views/organize_dialog.html.php @@ -15,22 +15,28 @@ </ul> </div> <div id="g-organize-detail" class="g-left ui-helper-clearfix"> - <div id="g-organize-microthumb-panel" class="ui-widget" + <ul id="g-action-status" class="g-message-block"> + <li class="g-info"><?= t("Drag and drop photos to re-order or move between albums") ?></li> + </ul> + <div id="g-organize-microthumb-grid" class="ui-widget" ref="<?= url::site("organize/album/__ITEM_ID__/__OFFSET__") ?>"> - <ul id="g-action-status" class="g-message-block"> - <li class="g-info"><?= t("Drag and drop photos to re-order or move between albums") ?></li> - </ul> - <ul id="g-organize-microthumb-grid" class="ui-widget-content"> <?= $micro_thumb_grid ?> - </ul> </div> <div id="g-organize-controls" class="ui-widget-header"> <a id="g-organize-close" href="#" ref="done" class="g-button g-right ui-corner-all ui-state-default"><?= t("Close") ?></a> <form> - <?= t("Sort order") ?> - <?= form::dropdown(array("id" => "g-organize-sort-column"), album::get_sort_order_options(), $album->sort_column) ?> - <?= form::dropdown(array("id" => "g-organize-sort-order"), array("ASC" => "Ascending", "DESC" => "Descending"), $album->sort_order) ?> + <ul> + <li id="g-organize-sort-order-text" class="g-left"><?= t("Sort order") ?></li> + <li class="g-left"> + <?= form::dropdown(array("id" => "g-organize-sort-column"), + album::get_sort_order_options(), $album->sort_column) ?> + </li> + <li class="g-left"> + <?= form::dropdown(array("id" => "g-organize-sort-order"), + array("ASC" => t("Ascending"), "DESC" => t("Descending")), $album->sort_order) ?> + </li> + </ul> </form> </div> </div> diff --git a/modules/organize/views/organize_thumb_grid.html.php b/modules/organize/views/organize_thumb_grid.html.php index 9a9cd819..f5db53d4 100644 --- a/modules/organize/views/organize_thumb_grid.html.php +++ b/modules/organize/views/organize_thumb_grid.html.php @@ -1,12 +1,10 @@ <?php defined("SYSPATH") or die("No direct script access.") ?> <? foreach ($album->children(25, $offset) as $child): ?> -<li class="g-organize-microthumb-grid-cell g-left ui-state-default" ref="<?= $child->id ?>"> - <div id="g-organize-microthumb-<?= $child->id ?>" - class="g-organize-microthumb <?= $child->is_album() ? "g-album" : "g-photo" ?> ui-state-active"> - <?= $child->thumb_img(array("class" => "g-thumbnail", "ref" => $child->id), 90, true) ?> - <span<?= $child->is_album() ? " class=\"ui-icon ui-icon-note\"" : "" ?>></span> - </div> -</li> +<div class="g-organize-microthumb-grid-cell g-left ui-state-default ui-state-active <?= $child->is_album() ? "g-album" : "g-photo" ?>" + ref="<?= $child->id ?>"> + <?= $child->thumb_img(array("class" => "g-thumbnail", "ref" => $child->id), 90, true) ?> + <span<?= $child->is_album() ? " class=\"ui-icon ui-icon-note\"" : "" ?>></span> +</div> <? endforeach ?> <? if ($album->children_count() > $offset): ?> diff --git a/modules/organize/views/organize_tree.html.php b/modules/organize/views/organize_tree.html.php index 513c0625..33d7b4c9 100644 --- a/modules/organize/views/organize_tree.html.php +++ b/modules/organize/views/organize_tree.html.php @@ -1,21 +1,21 @@ <?php defined("SYSPATH") or die("No direct script access.") ?> <li class="g-organize-album ui-icon-left <?= access::can("edit", $album) ? "" : "g-view-only" ?>" ref="<?= $album->id ?>"> - <span class="ui-icon ui-icon-minus"> + <span class="ui-icon ui-icon-minus g-left"> </span> - <span class="g-organize-album-text <?= $selected && $album->id == $selected->id ? "ui-state-focus" : "" ?>" + <span class="g-organize-album-text g-left <?= $selected && $album->id == $selected->id ? "ui-state-focus" : "" ?>" ref="<?= $album->id ?>"> <?= html::clean($album->title) ?> </span> - <ul> + <ul class="g-left"> <? foreach ($album->children(null, null, array(array("type", "=", "album"))) as $child): ?> <? if ($selected && $child->contains($selected)): ?> <?= View::factory("organize_tree.html", array("selected" => $selected, "album" => $child)); ?> <? else: ?> <li class="g-organize-album ui-icon-left <?= access::can("edit", $child) ? "" : "g-view-only" ?>" ref="<?= $child->id ?>"> - <span class="ui-icon ui-icon-plus"></span> - <span class="g-organize-album-text" ref="<?= $child->id ?>"> + <span class="ui-icon ui-icon-plus g-left"></span> + <span class="g-organize-album-text g-left <?= $selected && $child->id == $selected->id ? "ui-state-focus" : "" ?>" ref="<?= $child->id ?>"> <?= html::clean($child->title) ?> </span> </li> diff --git a/modules/rest/controllers/rest.php b/modules/rest/controllers/rest.php index 374ae0d2..7cdd97c9 100644 --- a/modules/rest/controllers/rest.php +++ b/modules/rest/controllers/rest.php @@ -22,7 +22,7 @@ class Rest_Controller extends Controller { $username = Input::instance()->post("user"); $password = Input::instance()->post("password"); - if (empty($username) || auth::too_many_failed_logins($username)) { + if (empty($username) || auth::too_many_failures($username)) { throw new Rest_Exception("Forbidden", 403); } diff --git a/modules/tag/controllers/tags.php b/modules/tag/controllers/tags.php index 1eede907..04400d73 100644 --- a/modules/tag/controllers/tags.php +++ b/modules/tag/controllers/tags.php @@ -60,7 +60,7 @@ class Tags_Controller extends Controller { $form = tag::get_add_form($item); if ($form->validate()) { - foreach (split(",", $form->add_tag->inputs["name"]->value) as $tag_name) { + foreach (explode(",", $form->add_tag->inputs["name"]->value) as $tag_name) { $tag_name = trim($tag_name); if ($tag_name) { $tag = tag::add($item, $tag_name); @@ -77,9 +77,9 @@ class Tags_Controller extends Controller { public function autocomplete() { $tags = array(); - $tag_parts = preg_split("#,#", Input::instance()->get("q")); + $tag_parts = explode(",", Input::instance()->get("q")); $limit = Input::instance()->get("limit"); - $tag_part = end($tag_parts); + $tag_part = ltrim(end($tag_parts)); $tag_list = ORM::factory("tag") ->where("name", "LIKE", "{$tag_part}%") ->order_by("name", "ASC") diff --git a/modules/tag/helpers/tag_event.php b/modules/tag/helpers/tag_event.php index 403ccd52..10075c02 100644 --- a/modules/tag/helpers/tag_event.php +++ b/modules/tag/helpers/tag_event.php @@ -34,7 +34,7 @@ class tag_event_Core { if (!empty($iptc["2#025"])) { foreach($iptc["2#025"] as $tag) { $tag = str_replace("\0", "", $tag); - foreach (preg_split("/,/", $tag) as $word) { + foreach (explode(",", $tag) as $word) { $word = trim($word); if (function_exists("mb_detect_encoding") && mb_detect_encoding($word) != "UTF-8") { $word = utf8_encode($word); @@ -82,7 +82,7 @@ class tag_event_Core { static function item_edit_form_completed($item, $form) { tag::clear_all($item); - foreach (preg_split("/,/", $form->edit_item->tags->value) as $tag_name) { + foreach (explode(",", $form->edit_item->tags->value) as $tag_name) { if ($tag_name) { tag::add($item, trim($tag_name)); } @@ -124,7 +124,7 @@ class tag_event_Core { } static function add_photos_form_completed($album, $form) { - foreach (split(",", $form->add_photos->tags->value) as $tag_name) { + foreach (explode(",", $form->add_photos->tags->value) as $tag_name) { $tag_name = trim($tag_name); if ($tag_name) { $tag = tag::add($album, $tag_name); diff --git a/modules/tag/helpers/tag_item_rest.php b/modules/tag/helpers/tag_item_rest.php index 672cec53..fe07fefb 100644 --- a/modules/tag/helpers/tag_item_rest.php +++ b/modules/tag/helpers/tag_item_rest.php @@ -34,7 +34,7 @@ class tag_item_rest_Core { } static function resolve($tuple) { - list ($tag_id, $item_id) = split(",", $tuple); + list ($tag_id, $item_id) = explode(",", $tuple); $tag = ORM::factory("tag", $tag_id); $item = ORM::factory("item", $item_id); if (!$tag->loaded() || !$item->loaded() || !$tag->has($item) || !access::can("view", $item)) { diff --git a/modules/user/controllers/admin_users.php b/modules/user/controllers/admin_users.php index 48847433..df3d96c9 100644 --- a/modules/user/controllers/admin_users.php +++ b/modules/user/controllers/admin_users.php @@ -60,9 +60,7 @@ class Admin_Users_Controller extends Admin_Controller { } public function add_user_form() { - $v = new View("user_form.html"); - $v->form = $this->_get_user_add_form_admin(); - print $v; + print $this->_get_user_add_form_admin(); } public function delete_user($id) { @@ -147,13 +145,7 @@ class Admin_Users_Controller extends Admin_Controller { throw new Kohana_404_Exception(); } - $v = new View("user_form.html"); - $v->form = $this->_get_user_edit_form_admin($user); - // Don't allow the user to control their own admin bit, else you can lock yourself out - if ($user->id == identity::active_user()->id) { - $v->form->edit_user->admin->disabled(1); - } - print $v; + print $this->_get_user_edit_form_admin($user); } public function add_user_to_group($user_id, $group_id) { @@ -293,6 +285,9 @@ class Admin_Users_Controller extends Admin_Controller { ->error_messages("length", t("This name is too long")); $group->password("password")->label(t("Password"))->id("g-password") ->error_messages("min_length", t("This password is too short")); + $group->script("") + ->text( + '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});'); $group->password("password2")->label(t("Confirm password"))->id("g-password2") ->error_messages("matches", t("The passwords you entered do not match")) ->matches($group->password); @@ -305,6 +300,11 @@ class Admin_Users_Controller extends Admin_Controller { self::_add_locale_dropdown($group, $user); $group->checkbox("admin")->label(t("Admin"))->id("g-admin")->checked($user->admin); + // Don't allow the user to control their own admin bit, else you can lock yourself out + if ($user->id == identity::active_user()->id) { + $group->admin->disabled(1); + } + module::event("user_edit_form_admin", $user, $form); $group->submit("")->value(t("Modify User")); return $form; @@ -321,6 +321,9 @@ class Admin_Users_Controller extends Admin_Controller { ->error_messages("length", t("This name is too long")); $group->password("password")->label(t("Password"))->id("g-password") ->error_messages("min_length", t("This password is too short")); + $group->script("") + ->text( + '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});'); $group->password("password2")->label(t("Confirm password"))->id("g-password2") ->error_messages("matches", t("The passwords you entered do not match")) ->matches($group->password); diff --git a/modules/user/controllers/password.php b/modules/user/controllers/password.php index 8309d2cc..07fdc1ed 100644 --- a/modules/user/controllers/password.php +++ b/modules/user/controllers/password.php @@ -110,7 +110,7 @@ class Password_Controller extends Controller { "mistyped", t("The password and the confirm password must match")); $group->submit("")->value(t("Update")); - $template->content = new View("user_form.html"); + $template->content = new View("confirm_reset_password.html"); $template->content->form = $form; return $template; } diff --git a/modules/user/controllers/users.php b/modules/user/controllers/users.php index 166ff8b2..0730f391 100644 --- a/modules/user/controllers/users.php +++ b/modules/user/controllers/users.php @@ -28,7 +28,6 @@ class Users_Controller extends Controller { try { $valid = $form->validate(); $user->full_name = $form->edit_user->full_name->value; - $user->email = $form->edit_user->email->value; $user->url = $form->edit_user->url->value; if ($user->locale != $form->edit_user->locale->value) { @@ -85,6 +84,7 @@ class Users_Controller extends Controller { $user->save(); module::event("user_change_password_form_completed", $user, $form); message::success(t("Password changed")); + module::event("user_auth", $user); module::event("user_password_change", $user); print json_encode( array("result" => "success", @@ -92,7 +92,42 @@ class Users_Controller extends Controller { } else { log::warning("user", t("Failed password change for %name", array("name" => $user->name))); $name = $user->name; - module::event("user_password_change_failed", $name); + module::event("user_auth_failed", $name); + print json_encode(array("result" => "error", "form" => (string) $form)); + } + } + + public function change_email($id) { + $user = user::lookup($id); + if ($user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + $form = $this->_get_change_email_form($user); + try { + $valid = $form->validate(); + $user->email = $form->change_email->email->value; + $user->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->change_email->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $user->save(); + module::event("user_change_email_form_completed", $user, $form); + message::success(t("Email address changed")); + module::event("user_auth", $user); + print json_encode( + array("result" => "success", + "resource" => url::site("users/{$user->id}"))); + } else { + log::warning("user", t("Failed email change for %name", array("name" => $user->name))); + $name = $user->name; + module::event("user_auth_failed", $name); print json_encode(array("result" => "error", "form" => (string) $form)); } } @@ -115,16 +150,25 @@ class Users_Controller extends Controller { print $this->_get_change_password_form($user); } + public function form_change_email($id) { + $user = user::lookup($id); + if ($user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + print $this->_get_change_email_form($user); + } + private function _get_change_password_form($user) { $form = new Forge( "users/change_password/$user->id", "", "post", array("id" => "g-change-password-user-form")); $group = $form->group("change_password")->label(t("Change your password")); $group->password("old_password")->label(t("Old password"))->id("g-password") - ->callback("auth::validate_too_many_failed_password_changes") + ->callback("auth::validate_too_many_failed_auth_attempts") ->callback("user::valid_password") ->error_messages("invalid", t("Incorrect password")) ->error_messages( - "too_many_failed_password_changes", + "too_many_failed_auth_attempts", t("Too many incorrect passwords. Try again later")); $group->password("password")->label(t("New password"))->id("g-password") ->error_messages("min_length", t("Your new password is too short")); @@ -140,16 +184,33 @@ class Users_Controller extends Controller { return $form; } + private function _get_change_email_form($user) { + $form = new Forge( + "users/change_email/$user->id", "", "post", array("id" => "g-change-email-user-form")); + $group = $form->group("change_email")->label(t("Change your email address")); + $group->password("password")->label(t("Current password"))->id("g-password") + ->callback("auth::validate_too_many_failed_auth_attempts") + ->callback("user::valid_password") + ->error_messages("invalid", t("Incorrect password")) + ->error_messages( + "too_many_failed_auth_attempts", + t("Too many incorrect passwords. Try again later")); + $group->input("email")->label(t("New email address"))->id("g-email")->value($user->email) + ->error_messages("email", t("You must enter a valid email address")) + ->error_messages("length", t("Your email address is too long")) + ->error_messages("required", t("You must enter a valid email address")); + + module::event("user_change_password_form", $user, $form); + $group->submit("")->value(t("Save")); + return $form; + } + private function _get_edit_form($user) { $form = new Forge("users/update/$user->id", "", "post", array("id" => "g-edit-user-form")); $group = $form->group("edit_user")->label(t("Edit your profile")); $group->input("full_name")->label(t("Full Name"))->id("g-fullname")->value($user->full_name) ->error_messages("length", t("Your name is too long")); self::_add_locale_dropdown($group, $user); - $group->input("email")->label(t("Email"))->id("g-email")->value($user->email) - ->error_messages("email", t("You must enter a valid email address")) - ->error_messages("length", t("Your email address is too long")) - ->error_messages("required", t("You must enter a valid email address")); $group->input("url")->label(t("URL"))->id("g-url")->value($user->url); module::event("user_edit_form", $user, $form); diff --git a/modules/user/tests/No_Direct_ORM_Access_Test.php b/modules/user/tests/No_Direct_ORM_Access_Test.php index c372258e..eb7f09b0 100644 --- a/modules/user/tests/No_Direct_ORM_Access_Test.php +++ b/modules/user/tests/No_Direct_ORM_Access_Test.php @@ -31,7 +31,7 @@ class No_Direct_ORM_Access_Test extends Gallery_Unit_Test_Case { //if (basename(dirname($file)) == "helpers") { $file_as_string = file_get_contents($file); if (preg_match("/ORM::factory\\(\"user\"/", $file_as_string)) { - foreach (split("\n", $file_as_string) as $l => $line) { + foreach (explode("\n", $file_as_string) as $l => $line) { if (preg_match('/ORM::factory\\(\"user\"/', $line)) { $errors[] = "$file($l) => $line"; } @@ -54,7 +54,7 @@ class No_Direct_ORM_Access_Test extends Gallery_Unit_Test_Case { foreach ($dir as $file) { $file_as_string = file_get_contents($file); if (preg_match("/ORM::factory\\(\"group\"/", $file_as_string)) { - foreach (split("\n", $file_as_string) as $l => $line) { + foreach (explode("\n", $file_as_string) as $l => $line) { if (preg_match('/ORM::factory\\(\"group\"/', $line)) { $errors[] = "$file($l) => $line"; } diff --git a/modules/user/views/confirm_reset_password.html.php b/modules/user/views/confirm_reset_password.html.php new file mode 100644 index 00000000..4993189e --- /dev/null +++ b/modules/user/views/confirm_reset_password.html.php @@ -0,0 +1,2 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= $form ?>
\ No newline at end of file diff --git a/themes/wind/css/screen.css b/themes/wind/css/screen.css index 12fa695d..630e8bbd 100644 --- a/themes/wind/css/screen.css +++ b/themes/wind/css/screen.css @@ -447,3 +447,4 @@ td { .rtl #g-view-menu #g-slideshow-link { background-image: url('../images/ico-view-slideshow-rtl.png'); } + diff --git a/themes/wind/views/no_sidebar.html.php b/themes/wind/views/no_sidebar.html.php index 378bd971..83686318 100644 --- a/themes/wind/views/no_sidebar.html.php +++ b/themes/wind/views/no_sidebar.html.php @@ -1,6 +1,6 @@ <?php defined("SYSPATH") or die("No direct script access.") ?> <ul class="g-message-block"> - <li class="g-warning"><?= t("No active sidebar blocks.<br/> - <a href=\"%url\">Add blocks</a>", - array("url" => html::mark_clean(url::site("admin/sidebar")))) ?></li> + <li class="g-warning"><?= t("No active sidebar blocks." ?> + <br/><a href="<?= url::site("admin/sidebar") ?>"><?= t("Add blocks") ?></a> + </li> </ul> |