diff options
| author | Tristan Zur <tzur@webserver.ccwn.org> | 2015-06-10 20:55:53 +0200 |
|---|---|---|
| committer | Tristan Zur <tzur@webserver.ccwn.org> | 2015-06-10 20:55:53 +0200 |
| commit | 406abd7c4df1ace2cd3e4e17159e8941a2e8c0c4 (patch) | |
| tree | a324be16021f44f2fd6d55e609f47024e945b1db /modules | |
Initial import
Diffstat (limited to 'modules')
506 files changed, 53859 insertions, 0 deletions
diff --git a/modules/akismet/controllers/admin_akismet.php b/modules/akismet/controllers/admin_akismet.php new file mode 100644 index 0000000..c907966 --- /dev/null +++ b/modules/akismet/controllers/admin_akismet.php @@ -0,0 +1,67 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Akismet_Controller extends Admin_Controller { + public function index() { + $form = akismet::get_configure_form(); + + if (request::method() == "post") { + // @todo move the "post" handler part of this code into a separate function + access::verify_csrf(); + + if ($form->validate()) { + $new_key = $form->configure_akismet->api_key->value; + $old_key = module::get_var("akismet", "api_key"); + if ($old_key && !$new_key) { + message::success(t("Your Akismet key has been cleared.")); + } else if ($old_key && $new_key && $old_key != $new_key) { + message::success(t("Your Akismet key has been changed.")); + } else if (!$old_key && $new_key) { + message::success(t("Your Akismet key has been saved.")); + } + + log::success("akismet", t("Akismet key changed to %new_key", + array("new_key" => $new_key))); + module::set_var("akismet", "api_key", $new_key); + akismet::check_config(); + url::redirect("admin/akismet"); + } else { + $valid_key = false; + } + } else { + $valid_key = module::get_var("akismet", "api_key") ? 1 : 0; + } + + akismet::check_config(); + $view = new Admin_View("admin.html"); + $view->page_title = t("Akismet spam filtering"); + $view->content = new View("admin_akismet.html"); + $view->content->valid_key = $valid_key; + $view->content->form = $form; + print $view; + } + + public function stats() { + $view = new Admin_View("admin.html"); + $view->content = new View("admin_akismet_stats.html"); + $view->content->api_key = module::get_var("akismet", "api_key"); + $view->content->blog_url = url::base(false, "http"); + print $view; + } +}
\ No newline at end of file diff --git a/modules/akismet/helpers/akismet.php b/modules/akismet/helpers/akismet.php new file mode 100644 index 0000000..a7927d6 --- /dev/null +++ b/modules/akismet/helpers/akismet.php @@ -0,0 +1,193 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class akismet_Core { + public static $test_mode = TEST_MODE; + + static function get_configure_form() { + $form = new Forge("admin/akismet", "", "post", array("id" => "g-configure-akismet-form")); + $group = $form->group("configure_akismet")->label(t("Configure Akismet")); + $group->input("api_key")->label(t("API Key"))->value(module::get_var("akismet", "api_key")) + ->callback("akismet::validate_key") + ->error_messages("invalid", t("The API key you provided is invalid.")); + $group->submit("")->value(t("Save")); + return $form; + } + + /** + * Check a comment against Akismet and return "spam", "ham" or "unknown". + * @param Comment_Model $comment A comment to check + * @return $string "spam", "ham" or "unknown" + */ + static function check_comment($comment) { + if (akismet::$test_mode) { + return; + } + + $request = self::_build_request("comment-check", $comment); + $response = self::_http_post($request); + $answer = $response->body[0]; + if ($answer == "true") { + return "spam"; + } else if ($answer == "false") { + return "ham"; + } else { + return "unknown"; + } + } + + /** + * Tell Akismet that this comment is spam + * @param Comment_Model $comment A comment to check + */ + static function submit_spam($comment) { + if (akismet::$test_mode) { + return; + } + + $request = self::_build_request("submit-spam", $comment); + self::_http_post($request); + } + + /** + * Tell Akismet that this comment is ham + * @param Comment_Model $comment A comment to check + */ + static function submit_ham($comment) { + if (akismet::$test_mode) { + return; + } + + $request = self::_build_request("submit-ham", $comment); + self::_http_post($request); + } + + /** + * Check an API Key against Akismet to make sure that it's valid + * @param string $api_key the API key + * @return boolean + */ + static function validate_key($api_key_input) { + if ($api_key_input->value) { + $request = self::_build_verify_request($api_key_input->value); + $response = self::_http_post($request, "rest.akismet.com"); + if ("valid" != $response->body[0]) { + $api_key_input->add_error("invalid", 1); + } + } + } + + + static function check_config() { + $api_key = module::get_var("akismet", "api_key"); + if (empty($api_key)) { + site_status::warning( + t("Akismet is not quite ready! Please provide an <a href=\"%url\">API Key</a>", + array("url" => html::mark_clean(url::site("admin/akismet")))), + "akismet_config"); + } else { + site_status::clear("akismet_config"); + } + } + + + static function _build_verify_request($api_key) { + $base_url = url::base(false, "http"); + $query_string = "key={$api_key}&blog=$base_url"; + + $version = module::get_version("akismet"); + $http_request = "POST /1.1/verify-key HTTP/1.0\r\n"; + $http_request .= "Host: rest.akismet.com\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; + $http_request .= "Content-Length: " . strlen($query_string) . "\r\n"; + $http_request .= "User-Agent: Gallery/3 | Akismet/$version\r\n"; + $http_request .= "\r\n"; + $http_request .= $query_string; + + return $http_request; + } + + static function _build_request($function, $comment) { + $comment_data = array(); + $comment_data["HTTP_ACCEPT"] = $comment->server_http_accept; + $comment_data["HTTP_ACCEPT_ENCODING"] = $comment->server_http_accept_encoding; + $comment_data["HTTP_ACCEPT_LANGUAGE"] = $comment->server_http_accept_language; + $comment_data["HTTP_CONNECTION"] = $comment->server_http_connection; + $comment_data["HTTP_HOST"] = $comment->server_http_host; + $comment_data["HTTP_USER_AGENT"] = $comment->server_http_user_agent; + $comment_data["QUERY_STRING"] = $comment->server_query_string; + $comment_data["REMOTE_ADDR"] = $comment->server_remote_addr; + $comment_data["REMOTE_HOST"] = $comment->server_remote_host; + $comment_data["REMOTE_PORT"] = $comment->server_remote_port; + $comment_data["SERVER_HTTP_ACCEPT_CHARSET"] = $comment->server_http_accept_charset; + $comment_data["blog"] = url::base(false, "http"); + $comment_data["comment_author"] = $comment->author_name(); + $comment_data["comment_author_email"] = $comment->author_email(); + $comment_data["comment_author_url"] = $comment->author_url(); + $comment_data["comment_content"] = $comment->text; + $comment_data["comment_type"] = "comment"; + $comment_data["permalink"] = url::site("comments/{$comment->id}"); + $comment_data["referrer"] = $comment->server_http_referer; + $comment_data["user_agent"] = $comment->server_http_user_agent; + $comment_data["user_ip"] = $comment->server_remote_addr; + + $query_string = array(); + foreach ($comment_data as $key => $data) { + $query_string[] = "$key=" . urlencode($data); + } + $query_string = join("&", $query_string); + + $version = module::get_version("akismet"); + $http_request = "POST /1.1/$function HTTP/1.0\r\n"; + $http_request .= "Host: " . module::get_var("akismet", "api_key") . ".rest.akismet.com\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; + $http_request .= "Content-Length: " . strlen($query_string) . "\r\n"; + $http_request .= "User-Agent: Gallery/3 | Akismet/$version\r\n"; + $http_request .= "\r\n"; + $http_request .= $query_string; + + return $http_request; + } + + private static function _http_post($http_request, $host=null) { + if (!$host) { + $host = module::get_var("akismet", "api_key") . ".rest.akismet.com"; + } + $response = ""; + + Kohana_Log::add("debug", "Send request\n" . print_r($http_request, 1)); + if (false !== ($fs = @fsockopen($host, 80, $errno, $errstr, 5))) { + fwrite($fs, $http_request); + while ( !feof($fs) ) { + $response .= fgets($fs, 1160); // One TCP-IP packet + } + fclose($fs); + list($headers, $body) = explode("\r\n\r\n", $response); + $headers = explode("\r\n", $headers); + $body = explode("\r\n", $body); + $response = new ArrayObject( + array("headers" => $headers, "body" => $body), ArrayObject::ARRAY_AS_PROPS); + } else { + throw new Exception("@todo CONNECTION TO SPAM SERVICE FAILED"); + } + Kohana_Log::add("debug", "Received response\n" . print_r($response, 1)); + + return $response; + } +} diff --git a/modules/akismet/helpers/akismet_event.php b/modules/akismet/helpers/akismet_event.php new file mode 100644 index 0000000..038e487 --- /dev/null +++ b/modules/akismet/helpers/akismet_event.php @@ -0,0 +1,70 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class akismet_event_Core { + static function comment_created($comment) { + if (!module::get_var("akismet", "api_key")) { + return; + } + + switch(akismet::check_comment($comment)) { + case "spam": + $comment->state = "spam"; + module::incr_var("comment", "spam_caught"); + break; + + case "ham": + $comment->state = "published"; + break; + + case "unknown": + $comment->state = "unpublished"; + break; + } + $comment->save(); + } + + static function comment_updated($original, $new) { + if (!module::get_var("akismet", "api_key")) { + return; + } + + if ($original->state != "spam" && $new->state == "spam") { + akismet::submit_spam($new); + } else if ($original->state == "spam" && $new->state != "spam") { + akismet::submit_ham($new); + } + } + + static function admin_menu($menu, $theme) { + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("akismet") + ->label(t("Akismet")) + ->url(url::site("admin/akismet"))); + + if (module::get_var("akismet", "api_key")) { + $menu->get("statistics_menu") + ->append(Menu::factory("link") + ->id("akismet") + ->label(t("Akismet")) + ->url(url::site("admin/akismet/stats"))); + } + } +} diff --git a/modules/akismet/helpers/akismet_installer.php b/modules/akismet/helpers/akismet_installer.php new file mode 100644 index 0000000..bd16a15 --- /dev/null +++ b/modules/akismet/helpers/akismet_installer.php @@ -0,0 +1,28 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class akismet_installer { + static function activate() { + akismet::check_config(); + } + + static function deactivate() { + site_status::clear("akismet_config"); + } +} diff --git a/modules/akismet/module.info b/modules/akismet/module.info new file mode 100644 index 0000000..263b7b8 --- /dev/null +++ b/modules/akismet/module.info @@ -0,0 +1,7 @@ +name = "Akismet" +description = "Filter comments through the Akismet web service to detect and eliminate spam (http://akismet.com). You'll need a WordPress.com API key to use it." +version = 1 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:akismet" +discuss_url = "http://galleryproject.org/forum_module_akismet" diff --git a/modules/akismet/views/admin_akismet.html.php b/modules/akismet/views/admin_akismet.html.php new file mode 100644 index 0000000..399053d --- /dev/null +++ b/modules/akismet/views/admin_akismet.html.php @@ -0,0 +1,18 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-admin-akismet" class="g-block"> + <h1> <?= t("Akismet spam filtering") ?> </h1> + <p> + <?= t("Akismet is a free, automated spam filtering service. In order to use it, you need to sign up for a <a href=\"%api_key_url\">Wordpress.com API Key</a>, which is also free. Your comments will be automatically relayed to <a href=\"%akismet_url\">Akismet.com</a> where they'll be scanned for spam. Spam messages will be flagged accordingly and hidden from your vistors until you approve or delete them.", + array("api_key_url" => "http://wordpress.com/api-keys", + "akismet_url" => "http://akismet.com")) ?> + </p> + <div class="g-block-content"> + <? if ($valid_key): ?> + <div class="g-module-status g-success"> + <?= t("Your API key is valid. Your comments will be filtered!") ?> + </div> + <? endif ?> + + <?= $form ?> + </div> +</div> diff --git a/modules/akismet/views/admin_akismet_stats.html.php b/modules/akismet/views/admin_akismet_stats.html.php new file mode 100644 index 0000000..32908ba --- /dev/null +++ b/modules/akismet/views/admin_akismet_stats.html.php @@ -0,0 +1,11 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javscript"> + $(document).ready(function() { + $("#g-akismet-external-stats").css("height", "600"); + }); +</script> +<div id="g-akismet-stats"> + <iframe id="g-akismet-external-stats" width="100%" height="500" frameborder="0" + src="http://<?= $api_key ?>.web.akismet.com/1.0/user-stats.php?blog=<?= urlencode($blog_url) ?>"> + </iframe> +</div> diff --git a/modules/calendarview/controllers/calendarview.php b/modules/calendarview/controllers/calendarview.php new file mode 100644 index 0000000..b1bae80 --- /dev/null +++ b/modules/calendarview/controllers/calendarview.php @@ -0,0 +1,296 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class CalendarView_Controller extends Controller { + public function calendar($display_year="", $display_user="-1") { + // Draw a calendar for the year specified by $display_year. + + // Display the current year by default if a year wasn't provided. + if ($display_year == "") { + $display_year = date('Y'); + } + + // Draw the page. + $root = item::root(); + $template = new Theme_View("page.html", "other", "CalendarView"); + $template->set_global( + array("calendar_user" => $display_user, + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($display_year, url::site("calendarview/calendar/" . $display_year))->set_last()))); + $template->page_title = t("Gallery :: Calendar"); + $template->content = new View("calendarview_year.html"); + $template->content->calendar_year = $display_year; + $template->content->calendar_user = $display_user; + $template->content->calendar_user_year_form = $this->_get_calenderprefs_form($display_year, $display_user); + $template->content->title = t("Calendar") . ": " . $display_year; + print $template; + } + + public function day($display_year, $display_user, $display_month, $display_day) { + // Display all images for the specified day. + + // Set up default search conditions for retrieving all photos from the specified day. + $where = array(array("type", "!=", "album")); + if ($display_user != "-1") { + $where[] = array("owner_id", "=", $display_user); + } + $where[] = array("captured", ">=", mktime(0, 0, 0, $display_month, $display_day, $display_year)); + $where[] = array("captured", "<", mktime(0, 0, 0, $display_month, ($display_day + 1), $display_year)); + + // Figure out the total number of photos to display. + $day_count = calendarview::get_items_count($where); + + // Figure out paging stuff. + $page_size = module::get_var("gallery", "page_size", 9); + $page = (int) Input::instance()->get("page", "1"); + $offset = ($page-1) * $page_size; + $max_pages = max(ceil($day_count / $page_size), 1); + + // Make sure that the page references a valid offset + if (($page < 1) || ($page > $max_pages)) { + throw new Kohana_404_Exception(); + } + + // Figure out which photos go on this page. + $children = calendarview::get_items($page_size, $offset, $where); + + // Create and display the page. + $root = item::root(); + $template = new Theme_View("page.html", "collection", "CalendarDayView"); + $template->set_global( + array("page" => $page, + "max_pages" => $max_pages, + "page_size" => $page_size, + "children" => $children, + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($display_year, url::site("calendarview/calendar/" . $display_year . "/" . $display_user)), + Breadcrumb::instance(t(date("F", mktime(0, 0, 0, $display_month, $display_day, $display_year))), url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month)), + Breadcrumb::instance($display_day, url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month . "/" . $display_day))->set_last()), + "children_count" => $day_count)); + $template->page_title = t("Gallery :: Calendar"); + $template->content = new View("dynamic.html"); + $template->content->title = t("Photos From ") . date("d", mktime(0, 0, 0, $display_month, $display_day, $display_year)) . " " . t(date("F", mktime(0, 0, 0, $display_month, $display_day, $display_year))) . " " . date("Y", mktime(0, 0, 0, $display_month, $display_day, $display_year)); + print $template; + + // Set breadcrumbs on the photo pages to point back to the calendar day view. + item::set_display_context_callback("CalendarView_Controller::get_display_day_context", $display_user, $display_year, $display_month, $display_day); + } + + static function get_display_day_context($item, $display_user, $display_year, $display_month, $display_day) { + // Set up default search conditions for retrieving all photos from the specified day. + $where = array(array("type", "!=", "album")); + if ($display_user != "-1") { + $where[] = array("owner_id", "=", $display_user); + } + $where[] = array("captured", ">=", mktime(0, 0, 0, $display_month, $display_day, $display_year)); + $where[] = array("captured", "<", mktime(0, 0, 0, $display_month, ($display_day + 1), $display_year)); + + // Generate breadcrumbs for the photo page. + $root = item::root(); + $breadcrumbs = array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($display_year, url::site("calendarview/calendar/" . $display_year . "/" . $display_user)), + Breadcrumb::instance(t(date("F", mktime(0, 0, 0, $display_month, $display_day, $display_year))), url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month)), + Breadcrumb::instance($display_day, url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month . "/" . $display_day)), + Breadcrumb::instance($item->title, $item->url())->set_last() + ); + + return CalendarView_Controller::get_display_context($item, $where, $breadcrumbs); + } + + public function month($display_year, $display_user, $display_month) { + // Display all images for the specified month. + + // Set up default search conditions for retrieving all photos from the specified month. + $where = array(array("type", "!=", "album")); + if ($display_user != "-1") { + $where[] = array("owner_id", "=", $display_user); + } + $where[] = array("captured", ">=", mktime(0, 0, 0, $display_month, 1, $display_year)); + $where[] = array("captured", "<", mktime(0, 0, 0, $display_month+1, 1, $display_year)); + + // Figure out the total number of photos to display. + $day_count = calendarview::get_items_count($where); + + // Figure out paging stuff. + $page_size = module::get_var("gallery", "page_size", 9); + $page = (int) Input::instance()->get("page", "1"); + $offset = ($page-1) * $page_size; + $max_pages = max(ceil($day_count / $page_size), 1); + + // Make sure that the page references a valid offset + if (($page < 1) || ($page > $max_pages)) { + throw new Kohana_404_Exception(); + } + + // Figure out which photos go on this page. + $children = calendarview::get_items($page_size, $offset, $where); + + // Create and display the page. + $root = item::root(); + $template = new Theme_View("page.html", "collection", "CalendarMonthView"); + $template->set_global( + array("page" => $page, + "max_pages" => $max_pages, + "page_size" => $page_size, + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($display_year, url::site("calendarview/calendar/" . $display_year . "/" . $display_user)), + Breadcrumb::instance(t(date("F", mktime(0, 0, 0, $display_month, 1, $display_year))), url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month))->set_last()), + "children" => $children, + "children_count" => $day_count)); + $template->page_title = t("Gallery :: Calendar"); + $template->content = new View("dynamic.html"); + $template->content->title = t("Photos From ") . t(date("F", mktime(0, 0, 0, $display_month, 1, $display_year))) . " " . date("Y", mktime(0, 0, 0, $display_month, 1, $display_year)); + print $template; + + // Set up breadcrumbs for the photo pages to point back to the calendar month view. + item::set_display_context_callback("CalendarView_Controller::get_display_month_context", $display_user, $display_year, $display_month); + } + + static function get_display_month_context($item, $display_user, $display_year, $display_month) { + // Set up default search conditions for retrieving all photos from the specified month. + $where = array(array("type", "!=", "album")); + if ($display_user != "-1") { + $where[] = array("owner_id", "=", $display_user); + } + $where[] = array("captured", ">=", mktime(0, 0, 0, $display_month, 1, $display_year)); + $where[] = array("captured", "<", mktime(0, 0, 0, $display_month+1, 1, $display_year)); + + // Generate breadcrumbs for the photo page. + $root = item::root(); + $breadcrumbs = array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($display_year, url::site("calendarview/calendar/" . $display_year . "/" . $display_user)), + Breadcrumb::instance(t(date("F", mktime(0, 0, 0, $display_month, 1, $display_year))), url::site("calendarview/month/" . $display_year . "/" . $display_user . "/" . $display_month)), + Breadcrumb::instance($item->title, $item->url())->set_last() + ); + + return CalendarView_Controller::get_display_context($item, $where, $breadcrumbs); + } + + private function get_display_context($item, $where=array(), $breadcrumbs=array()) { + // Set up previous / next / breadcrumbs / etc to point to CalendarView instead of the album. + + // Figure out the photo's position. + $position = calendarview::get_position($item, $where); + + // Figure out the previous and next items. + if ($position > 1) { + list ($previous_item, $ignore, $next_item) = calendarview::get_items(3, $position - 2, $where); + } else { + list ($next_item) = calendarview::get_items(1, $position, $where); + } + + // Figure out the total number of photos. + $sibling_count = calendarview::get_items_count($where); + + return array("position" => $position, + "previous_item" => $previous_item, + "next_item" => $next_item, + "sibling_count" => $sibling_count, + "breadcrumbs" => $breadcrumbs); + } + + private function _get_calenderprefs_form($display_year, $display_user) { + // Generate a form to allow the visitor to select a year and a gallery photo owner. + $calendar_group = new Forge("calendarview/setprefs", "", "post", + array("id" => "g-view-calendar-form")); + + // Generate a list of all Gallery users who have uploaded photos. + $valid_users[-1] = "(All Users)"; + $gallery_users = ORM::factory("user")->find_all(); + foreach ($gallery_users as $one_user) { + $count = ORM::factory("item") + ->viewable() + ->where("owner_id", "=", $one_user->id) + ->where("type", "!=", "album") + ->where("captured", "!=", "") + ->find_all() + ->count(); + if ($count > 0) { + $valid_users[$one_user->id] = $one_user->full_name; + } + } + + // Generate a list of years, starting with the year the earliest photo was + // taken, and ending with the year of the most recent photo. + $valid_years = Array(); + if ($display_user == -1) { + $all_photos = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", "!=", "") + ->order_by("captured", "DESC") + ->find_all(); + $counter = date('Y', $all_photos[count($all_photos)-1]->captured); + while ($counter <= date('Y', $all_photos[0]->captured)) { + $valid_years[$counter] = $counter; + $counter++; + } + } else { + $all_photos = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", "!=", "") + ->where("owner_id", "=", $display_user) + ->order_by("captured", "DESC") + ->find_all(); + $counter = date('Y', $all_photos[count($all_photos)-1]->captured); + while ($counter <= date('Y', $all_photos[0]->captured)) { + $valid_years[$counter] = $counter; + $counter++; + } + } + + // Create the form. + $calendar_group->dropdown('cal_user') + ->label(t("Display Photos From User: ")) + ->id('cal_user') + ->options($valid_users) + ->selected($display_user); + $calendar_group->dropdown('cal_year') + ->label(t("For Year: ")) + ->id('cal_year') + ->options($valid_years) + ->selected($display_year); + + // Add a save button to the form. + $calendar_group->submit("SaveSettings")->value(t("Go"))->id('cal_go'); + + // Return the newly generated form. + return $calendar_group; + } + + public function setprefs() { + // Change the calendar year and / or user. + + // Prevent Cross Site Request Forgery + access::verify_csrf(); + + // Get user specified settings. + $str_user_id = Input::instance()->post("cal_user"); + $str_year_id = Input::instance()->post("cal_year"); + + // redirect to the currect page. + url::redirect(url::site("calendarview/calendar/" . $str_year_id . "/" . $str_user_id, request::protocol())); + } +} diff --git a/modules/calendarview/css/calendarview_calendar.css b/modules/calendarview/css/calendarview_calendar.css new file mode 100644 index 0000000..82dfaa9 --- /dev/null +++ b/modules/calendarview/css/calendarview_calendar.css @@ -0,0 +1,59 @@ +/* Grid view ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +#g-calendar-grid { + position: relative; + align: center; + float: left; + width: 200px; + height: 220px; + margin: 10px 10px 10px 10px; +} + +#g-calendar-profile-grid { + position: relative; + align: center; + float: left; + width: 200px; + height: 145px; + margin: 10px 10px 10px 10px; +} + +/* Search form ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +#cal_user { + top: 0px; + left: 60px; + display: inline; +} +#cal_year { + top: 0px; + left: 240px; + display: inline; +} +#cal_go { + top: 0px; + left: 328px; + display: inline; +} + +/* Content ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +table.calendar { + text-align: center; +} + +table.calendar caption { + font-size: 1.5em; + padding: 0.2em; +} + +table.calendar th, table.calendar td { + padding: 0.2em; + border: 0px; +} + +table.calendar td:hover { + background: #ddf; +} + +/* For RTL Languages ~~~~~~~~~~~~~~~~~~~~~~~ */ +.rtl #g-calendar-grid { + float: right; +} diff --git a/modules/calendarview/css/calendarview_menu.css b/modules/calendarview/css/calendarview_menu.css new file mode 100644 index 0000000..600cc15 --- /dev/null +++ b/modules/calendarview/css/calendarview_menu.css @@ -0,0 +1,3 @@ +#g-view-menu #g-calendarview-link { + background-image: url('../images/ico-view-calendarview.png'); +} diff --git a/modules/calendarview/helpers/calendarview.php b/modules/calendarview/helpers/calendarview.php new file mode 100644 index 0000000..4807bc0 --- /dev/null +++ b/modules/calendarview/helpers/calendarview.php @@ -0,0 +1,48 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class calendarview_Core { + static function get_items_count($where=array()) { + // Returns the number of viewable items identified by $where. + return ORM::factory("item") + ->viewable() + ->merge_where($where) + ->order_by("captured", "ASC") + ->count_all(); + } + + static function get_items($limit=null, $offset=null, $where=array()) { + // Returns the items identified by $where, up to $limit, and starting at $offset. + return ORM::factory("item") + ->viewable() + ->merge_where($where) + ->order_by("captured", "ASC") + ->find_all($limit, $offset); + } + + static function get_position($item, $where=array()) { + // Get's $item's position within $where. + return ORM::factory("item") + ->viewable() + ->merge_where($where) + ->where("items.id", "<=", $item->id) + ->order_by("captured", "ASC") + ->count_all(); + } +} diff --git a/modules/calendarview/helpers/calendarview_block.php b/modules/calendarview/helpers/calendarview_block.php new file mode 100644 index 0000000..05e470d --- /dev/null +++ b/modules/calendarview/helpers/calendarview_block.php @@ -0,0 +1,72 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class calendarview_block_Core { + static function get_site_list() { + return array("calendarview_photo" => t("More Photos From This Date")); + } + + static function get($block_id, $theme) { + $block = ""; + + // Make sure the current page belongs to an item. + if (!$theme->item()) { + return; + } + $item = $theme->item; + + $display_date = ""; + if (isset($item->captured)) { + $display_date = $item->captured; + }elseif (isset($item->created)) { + $display_date = $item->created; + } + + // Make sure there are photo's to display. + $day_count = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", ">=", mktime(0, 0, 0, date("n", $display_date), date("j", $display_date), date("Y", $display_date))) + ->where("captured", "<", mktime(0, 0, 0, date("n", $display_date), date("j", $display_date)+1, date("Y", $display_date))) + ->find_all() + ->count(); + $month_count = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", ">=", mktime(0, 0, 0, date("n", $display_date), 1, date("Y", $display_date))) + ->where("captured", "<", mktime(0, 0, 0, date("n", $display_date)+1, 1, date("Y", $display_date))) + ->find_all() + ->count(); + + switch ($block_id) { + case "calendarview_photo": + if ( ($display_date != "") && (($day_count > 0) || ($month_count > 0)) ) { + $block = new Block(); + $block->css_id = "g-calendarview-sidebar"; + $block->title = t("Calendar"); + $block->content = new View("calendarview_sidebar.html"); + $block->content->date = $display_date; + $block->content->day_count = $day_count; + $block->content->month_count = $month_count; + } + break; + } + return $block; + } +} diff --git a/modules/calendarview/helpers/calendarview_event.php b/modules/calendarview/helpers/calendarview_event.php new file mode 100644 index 0000000..e98415c --- /dev/null +++ b/modules/calendarview/helpers/calendarview_event.php @@ -0,0 +1,99 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class calendarview_event_Core { + static function photo_menu($menu, $theme) { + $menu->append(Menu::factory("link") + ->id("calendarview") + ->label(t("View Calendar")) + ->url(url::site("calendarview/calendar/")) + ->css_id("g-calendarview-link")); + } + + static function movie_menu($menu, $theme) { + $menu->append(Menu::factory("link") + ->id("calendarview") + ->label(t("View Calendar")) + ->url(url::site("calendarview/calendar/")) + ->css_id("g-calendarview-link")); + } + + static function album_menu($menu, $theme) { + $menu->append(Menu::factory("link") + ->id("calendarview") + ->label(t("View Calendar")) + ->url(url::site("calendarview/calendar/")) + ->css_id("g-calendarview-link")); + } + + static function tag_menu($menu, $theme) { + $menu->append(Menu::factory("link") + ->id("calendarview") + ->label(t("View Calendar")) + ->url(url::site("calendarview/calendar/")) + ->css_id("g-calendarview-link")); + } + + static function pre_deactivate($data) { + // If the admin is about to deactivate EXIF, warn them that this module requires it. + if ($data->module == "exif") { + $data->messages["warn"][] = t("The CalendarView module requires the EXIF module."); + } + } + + static function module_change($changes) { + // If EXIF is deactivated, display a warning that it is required for this module to function properly. + if (!module::is_active("exif") || in_array("exif", $changes->deactivate)) { + site_status::warning( + t("The CalendarView module requires the EXIF module. " . + "<a href=\"%url\">Activate the EXIF module now</a>", + array("url" => html::mark_clean(url::site("admin/modules")))), + "calendarview_needs_exif"); + } else { + site_status::clear("calendarview_needs_exif"); + } + } + + static function show_user_profile($data) { + // Display a few months on the user profile screen. + $v = new View("user_profile_calendarview.html"); + $v->user_id = $data->user->id; + + // Figure out what month the users newest photo was taken it. + // Make that the last month to display. + // If a user hasn't uploaded anything, make the current month + // the last to be displayed. + $latest_photo = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", "!=", "") + ->where("owner_id", "=", $data->user->id) + ->order_by("captured", "DESC") + ->find_all(1); + if (count($latest_photo) > 0) { + $v->user_year = date('Y', $latest_photo[0]->captured); + $v->user_month = date('n', $latest_photo[0]->captured); + } else { + $v->user_year = date('Y'); + $v->user_month = date('n'); + } + + $data->content[] = (object) array("title" => t("User calendar"), "view" => $v); + } +} diff --git a/modules/calendarview/helpers/calendarview_installer.php b/modules/calendarview/helpers/calendarview_installer.php new file mode 100644 index 0000000..986a13f --- /dev/null +++ b/modules/calendarview/helpers/calendarview_installer.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class calendarview_installer { + static function install() { + module::set_version("calendarview", 1); + } + + static function deactivate() { + site_status::clear("calendarview_needs_exif"); + } + + static function can_activate() { + $messages = array(); + if (!module::is_active("exif")) { + $messages["warn"][] = t("The CalendarView module requires the EXIF module."); + } + return $messages; + } + + static function uninstall() { + module::delete("calendarview"); + } +} diff --git a/modules/calendarview/helpers/calendarview_theme.php b/modules/calendarview/helpers/calendarview_theme.php new file mode 100644 index 0000000..d299bf3 --- /dev/null +++ b/modules/calendarview/helpers/calendarview_theme.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class calendarview_theme_Core { + static function head($theme) { + return $theme->css("calendarview_calendar.css") . + $theme->css("calendarview_menu.css"); + } +} diff --git a/modules/calendarview/images/ico-view-calendarview.png b/modules/calendarview/images/ico-view-calendarview.png Binary files differnew file mode 100644 index 0000000..5e564b8 --- /dev/null +++ b/modules/calendarview/images/ico-view-calendarview.png diff --git a/modules/calendarview/libraries/PHPCalendar.php b/modules/calendarview/libraries/PHPCalendar.php new file mode 100644 index 0000000..2d1df2b --- /dev/null +++ b/modules/calendarview/libraries/PHPCalendar.php @@ -0,0 +1,87 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); + +class PHPCalendar_Core { + // Month and year to use for calendaring + protected $month; + protected $year; + protected $month_url; + + // First Day of the Week (0 = Sunday or 1 = Monday). + protected $week_start = 0; + + // Events for the current month. + protected $event_data = Array(); + + public function __construct($month = NULL, $year = NULL, $url = NULL) + { + empty($month) and $month = date('n'); // Current month + empty($year) and $year = date('Y'); // Current year + + // Set the month and year + $this->month = (int) $month; + $this->year = (int) $year; + $this->month_url = $url; + } + + public function event($day_of_the_week, $event_url = NULL, $css_id = NULL, $custom_text = NULL) + { + $this->event_data += Array($day_of_the_week => Array($event_url, $css_id, $custom_text)); + } + + public function render() + { + return $this->generate_calendar($this->year, $this->month, $this->event_data, 2, $this->month_url, $this->week_start, NULL); + } + + # PHP Calendar (version 2.3), written by Keith Devens + # http://keithdevens.com/software/php_calendar + # see example at http://keithdevens.com/weblog + # License: http://keithdevens.com/software/license + function generate_calendar($year, $month, $days = array(), $day_name_length = 3, $month_href = NULL, $first_day = 0, $pn = array()) + { + $first_of_month = gmmktime(0,0,0,$month,1,$year); + #remember that mktime will automatically correct if invalid dates are entered + # for instance, mktime(0,0,0,12,32,1997) will be the date for Jan 1, 1998 + # this provides a built in "rounding" feature to generate_calendar() + + if ($first_day == 0) $day_names = array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"); + if ($first_day == 1) $day_names = array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"); + + list($month, $year, $month_name, $weekday) = explode(',',gmstrftime('%m,%Y,%B,%w',$first_of_month)); + $weekday = ($weekday + 7 - $first_day) % 7; #adjust for $first_day + $title = t(date("F", mktime(0, 0, 0, $month, 1, $year))) . ' ' . $year; + + #Begin calendar. Uses a real <caption>. See http://diveintomark.org/archives/2002/07/03 + @list($p, $pl) = each($pn); @list($n, $nl) = each($pn); #previous and next links, if applicable + if($p) $p = '<span class="calendar-prev">'.($pl ? '<a href="'.htmlspecialchars($pl).'">'.$p.'</a>' : $p).'</span> '; + if($n) $n = ' <span class="calendar-next">'.($nl ? '<a href="'.htmlspecialchars($nl).'">'.$n.'</a>' : $n).'</span>'; + $calendar = '<table class="calendar" id="g-calendar-month">'."\n". + '<td class="title" colspan="7" align="center">'.$p.($month_href ? '<a href="'. ($month_href) .'">'.$title.'</a>' : $title).$n."</td></tr>\n<tr>"; + + if($day_name_length){ #if the day names should be shown ($day_name_length > 0) + #if day_name_length is >3, the full name of the day will be printed + foreach($day_names as $d) + $calendar .= '<th abbr="' . $d .'">'.t($day_name_length < 4 ? substr($d,0,$day_name_length) : $d) . '</th>'; + $calendar .= "</tr>\n<tr>"; + } + + if($weekday > 0) $calendar .= '<td colspan="'.$weekday.'"> </td>'; #initial 'empty' days + for($day=1,$days_in_month=gmdate('t',$first_of_month); $day<=$days_in_month; $day++,$weekday++){ + if($weekday == 7){ + $weekday = 0; #start a new week + $calendar .= "</tr>\n<tr>"; + } + if(isset($days[$day]) and is_array($days[$day])){ + @list($link, $classes, $content) = $days[$day]; + if(is_null($content)) $content = $day; + $calendar .= '<td'.($classes ? ' class="'.htmlspecialchars($classes).'">' : '>'). + ($link ? '<a href="'.htmlspecialchars($link).'">'.$content.'</a>' : $content).'</td>'; + } + else $calendar .= "<td class=\"day\">$day</td>"; + } + if($weekday != 7) $calendar .= '<td colspan="'.(7-$weekday).'"> </td>'; #remaining "empty" days + + return $calendar."</tr>\n</table>\n"; + } +} +?>
\ No newline at end of file diff --git a/modules/calendarview/module.info b/modules/calendarview/module.info new file mode 100644 index 0000000..f51f162 --- /dev/null +++ b/modules/calendarview/module.info @@ -0,0 +1,7 @@ +name = "CalendarView" +description = "View your photos by the date they were taken." +version = 1 +author_name = "rWatcher" +author_url = "http://codex.gallery2.org/User:RWatcher" +info_url = "http://codex.gallery2.org/Gallery3:Modules:calendarview" +discuss_url = "http://gallery.menalto.com/node/92405" diff --git a/modules/calendarview/views/calendarview_sidebar.html.php b/modules/calendarview/views/calendarview_sidebar.html.php new file mode 100644 index 0000000..b4f1f6f --- /dev/null +++ b/modules/calendarview/views/calendarview_sidebar.html.php @@ -0,0 +1,9 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <? if ($day_count > 0): ?> + <li><a href="<?=url::site("calendarview/day/" . date("Y", $date) . "/-1/" . date("n", $date) . "/" . date("j", $date)); ?>"><?=t("More from"); ?> <?=date("F", $date); ?> <?=date("j", $date); ?><?=date("S", $date); ?></a></li> + <? endif ?> + <? if ($month_count > 0): ?> + <li><a href="<?=url::site("calendarview/month/" . date("Y", $date) . "/-1/" . date("n", $date)); ?>"><?=t("More from"); ?> <?=date("F", $date); ?></a></li> + <? endif ?> +</ul>
\ No newline at end of file diff --git a/modules/calendarview/views/calendarview_year.html.php b/modules/calendarview/views/calendarview_year.html.php new file mode 100644 index 0000000..b40d7d5 --- /dev/null +++ b/modules/calendarview/views/calendarview_year.html.php @@ -0,0 +1,95 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-album-header"> + <div id="g-album-header-buttons"> + <?= $theme->dynamic_top() ?> + </div> + <h1><?= html::clean($title) ?></h1> +</div> + +<br/><?= $calendar_user_year_form ?><br /><br /> + +<? + // Search the db for all photos that were taken during the selected year. + if ($calendar_user == "-1") { + $items_for_year = ORM::factory("item") + ->viewable() + ->where("type", "!=", "album") + ->where("captured", ">=", mktime(0, 0, 0, 1, 1, $calendar_year)) + ->where("captured", "<", mktime(0, 0, 0, 1, 1, ($calendar_year + 1))) + ->order_by("captured") + ->find_all(); + } else { + $items_for_year = ORM::factory("item") + ->viewable() + ->where("owner_id", "=", $calendar_user) + ->where("type", "!=", "album") + ->where("captured", ">=", mktime(0, 0, 0, 1, 1, $calendar_year)) + ->where("captured", "<", mktime(0, 0, 0, 1, 1, ($calendar_year + 1))) + ->order_by("captured") + ->find_all(); + } + + // Set up some initial variables. + $counter_months = 1; + $counter_days = 0; + $counter = 0; + + // Set up the January Calendar. + // Check and see if any photos were taken in January, + // If so, make the month title into a clickable link. + print "<div id=\"g-calendar-grid\">"; + if ((count($items_for_year) > 0) && (date("n", $items_for_year[$counter]->captured) == 1)) { + $month_url = url::site("calendarview/month/" . $calendar_year . "/" . $calendar_user . "/" . $counter_months . "/"); + } else { + $month_url = ""; + } + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + + // Loop through each photo taken during this year, and see what month and day they were taken on. + // Make the corresponding dates on the calendars into clickable links. + while ($counter < (count($items_for_year))) { + + // Check and see if we've switched to a new month. + // If so, render the current calendar and set up a new one. + while (date("n", $items_for_year[$counter]->captured) > $counter_months) { + echo $calendar->render(); + print "</div>"; + $counter_months++; + $counter_days = 0; + print "<div id=\"g-calendar-grid\">"; + if (date("n", $items_for_year[$counter]->captured) == $counter_months) { + $month_url = url::site("calendarview/month/" . $calendar_year . "/" . $calendar_user . "/" . $counter_months . "/"); + } else { + $month_url = ""; + } + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + } + + // If the day of the current photo is different then the day of the previous photo, + // then add a link to the calendar for this date and set the current day to this day. + if (date("j", $items_for_year[$counter]->captured) > $counter_days) { + $counter_days = date("j", $items_for_year[$counter]->captured); + $calendar->event($counter_days, url::site("calendarview/day/" . $calendar_year . "/" . $calendar_user . "/" . $counter_months . "/" . $counter_days)); + } + + // Move onto the next photo. + $counter++; + } + + // Print out the last calendar to be generated. + echo $calendar->render(); + print "</div>"; + $counter_months++; + + // If the calendar that was previously rendered was not December, + // then print out a few empty months for the rest of the year. + while ($counter_months < 13) { + print "<div id=\"g-calendar-grid\">"; + $month_url = ""; + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + echo $calendar->render(); + print "</div>"; + $counter_months++; + } +?> +<?= $theme->dynamic_bottom() ?> diff --git a/modules/calendarview/views/user_profile_calendarview.html.php b/modules/calendarview/views/user_profile_calendarview.html.php new file mode 100644 index 0000000..4d9d68a --- /dev/null +++ b/modules/calendarview/views/user_profile_calendarview.html.php @@ -0,0 +1,86 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? + // Generate a list of items within the specified 3 month time-frame. + $items = ORM::factory("item") + ->viewable() + ->where("owner_id", "=", $user_id) + ->where("type", "!=", "album") + ->where("captured", ">=", mktime(0, 0, 0, $user_month-2, 1, $user_year)) + ->where("captured", "<", mktime(0, 0, 0, $user_month+1, 1, ($user_year))) + ->order_by("captured") + ->find_all(); + + // Set up some initial variables. + $calendar_year = $user_year; + $counter_months = $user_month - 2; + if ($counter_months < 1) { + $counter_months += 12; + $calendar_year--; + } + $counter_days = 0; + $counter = 0; + + // Print the first month. + print "<div id=\"g-calendar-profile-grid\">"; + if ((count($items) > 0) && (date("n", $items[$counter]->captured) == $counter_months)) { + $month_url = url::site("calendarview/month/" . $calendar_year . "/" . $user_id . "/" . $counter_months . "/"); + } else { + $month_url = ""; + } + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + + // Loop through each photo taken during the 3 month time frame, and see what month and day they were taken on. + // Make the corresponding dates on the calendars into clickable links. + while ($counter < (count($items))) { + + // Check and see if we've switched to a new month. + // If so, render the current calendar and set up a new one. + // Continue printing empty months until we reach the next photo or the last month. + while (date("n", $items[$counter]->captured) != $counter_months) { + echo $calendar->render(); + print "</div>"; + $counter_months++; + if ($counter_months == 13) { + $counter_months = 1; + $calendar_year++; + } + $counter_days = 0; + print "<div id=\"g-calendar-profile-grid\">"; + if (date("n", $items[$counter]->captured) == $counter_months) { + $month_url = url::site("calendarview/month/" . $calendar_year . "/" . $user_id . "/" . $counter_months . "/"); + } else { + $month_url = ""; + } + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + } + + // If the day of the current photo is different then the day of the previous photo, + // then add a link to the calendar for this date and set the current day to this day. + if (date("j", $items[$counter]->captured) > $counter_days) { + $counter_days = date("j", $items[$counter]->captured); + $calendar->event($counter_days, url::site("calendarview/day/" . $calendar_year . "/" . $user_id . "/" . $counter_months . "/" . $counter_days)); + } + + // Move onto the next photo. + $counter++; + } + + // Print out the last calendar to be generated. + echo $calendar->render(); + print "</div>"; + $counter_months++; + + // If the calendar that was previously rendered was not the final month, + // then print out a few empty months to fill the remaining space. + while ($counter_months < $user_month + 1) { + print "<div id=\"g-calendar-profile-grid\">"; + $month_url = ""; + $calendar = new PHPCalendar($counter_months, $calendar_year, $month_url); + echo $calendar->render(); + print "</div>"; + $counter_months++; + } + +?> +<br clear="all" /><br /><br /> +<div align="right"><a href="<?=url::site("calendarview/calendar/{$user_year}/{$user_id}"); ?>"><?=t("View full calendar"); ?> >></a></div> diff --git a/modules/comment/controllers/admin_comments.php b/modules/comment/controllers/admin_comments.php new file mode 100644 index 0000000..3018340 --- /dev/null +++ b/modules/comment/controllers/admin_comments.php @@ -0,0 +1,60 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Comments_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Comment settings"); + $view->content = new View("admin_comments.html"); + $view->content->form = $this->_get_admin_form(); + print $view; + } + + public function save() { + access::verify_csrf(); + $form = $this->_get_admin_form(); + $form->validate(); + module::set_var("comment", "access_permissions", + $form->comment_settings->access_permissions->value); + module::set_var("comment", "rss_visible", + $form->comment_settings->rss_visible->value); + message::success(t("Comment settings updated")); + url::redirect("admin/comments"); + } + + private function _get_admin_form() { + $form = new Forge("admin/comments/save", "", "post", + array("id" => "g-comments-admin-form")); + $comment_settings = $form->group("comment_settings")->label(t("Permissions")); + $comment_settings->dropdown("access_permissions") + ->label(t("Who can leave comments?")) + ->options(array("everybody" => t("Everybody"), + "registered_users" => t("Only registered users"))) + ->selected(module::get_var("comment", "access_permissions")); + $comment_settings->dropdown("rss_visible") + ->label(t("Which RSS feeds can users see?")) + ->options(array("all" => t("All comment feeds"), + "newest" => t("New comments feed only"), + "per_item" => t("Comments on photos, movies and albums only"))) + ->selected(module::get_var("comment", "rss_visible")); + $comment_settings->submit("save")->value(t("Save")); + return $form; + } +} + diff --git a/modules/comment/controllers/admin_manage_comments.php b/modules/comment/controllers/admin_manage_comments.php new file mode 100644 index 0000000..ef31c95 --- /dev/null +++ b/modules/comment/controllers/admin_manage_comments.php @@ -0,0 +1,144 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Manage_Comments_Controller extends Admin_Controller { + private static $items_per_page = 20; + + public function index() { + // Get rid of old deleted/spam comments once in a while + db::build() + ->delete("comments") + ->where("state", "IN", array("deleted", "spam")) + ->where("updated", "<", db::expr("UNIX_TIMESTAMP() - 86400 * 7")) + ->execute(); + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_manage_comments.html"); + $view->content->menu = $this->_menu($this->_counts()); + print $view; + } + + public function menu_labels() { + $menu = $this->_menu($this->_counts()); + json::reply(array((string) $menu->get("unpublished")->label, + (string) $menu->get("published")->label, + (string) $menu->get("spam")->label, + (string) $menu->get("deleted")->label)); + } + + public function queue($state) { + $page = max(Input::instance()->get("page"), 1); + + $view = new Gallery_View("admin_manage_comments_queue.html"); + $view->counts = $this->_counts(); + $view->menu = $this->_menu($view->counts); + $view->state = $state; + $view->comments = ORM::factory("comment") + ->order_by("created", "DESC") + ->order_by("id", "DESC") + ->where("state", "=", $state) + ->limit(self::$items_per_page) + ->offset(($page - 1) * self::$items_per_page) + ->find_all(); + + // This view is not themed so we can't use $theme->url() in the view and have to + // reproduce Gallery_View::url() logic here. + $atn = theme::$admin_theme_name; + $view->fallback_avatar_url = url::abs_file("themes/$atn/images/avatar.jpg"); + + $view->page = $page; + $view->page_type = "collection"; + $view->page_subtype = "admin_comments"; + $view->page_size = self::$items_per_page; + $view->children_count = $this->_counts()->$state; + $view->max_pages = ceil($view->children_count / $view->page_size); + + // Also we want to use $theme->paginator() so we need a dummy theme + $view->theme = $view; + + print $view; + } + + private function _menu($counts) { + return Menu::factory("root") + ->append(Menu::factory("link") + ->id("unpublished") + ->label(t2("Awaiting Moderation (%count)", + "Awaiting Moderation (%count)", + $counts->unpublished)) + ->url(url::site("admin/manage_comments/queue/unpublished"))) + ->append(Menu::factory("link") + ->id("published") + ->label(t2("Approved (%count)", + "Approved (%count)", + $counts->published)) + ->url(url::site("admin/manage_comments/queue/published"))) + ->append(Menu::factory("link") + ->id("spam") + ->label(t2("Spam (%count)", + "Spam (%count)", + $counts->spam)) + ->url(url::site("admin/manage_comments/queue/spam"))) + ->append(Menu::factory("link") + ->id("deleted") + ->label(t2("Recently Deleted (%count)", + "Recently Deleted (%count)", + $counts->deleted)) + ->url(url::site("admin/manage_comments/queue/deleted"))); + } + + private function _counts() { + $counts = new stdClass(); + $counts->unpublished = 0; + $counts->published = 0; + $counts->spam = 0; + $counts->deleted = 0; + foreach (db::build() + ->select("state") + ->select(array("c" => 'COUNT("*")')) + ->from("comments") + ->group_by("state") + ->execute() as $row) { + $counts->{$row->state} = $row->c; + } + return $counts; + } + + public function set_state($id, $state) { + access::verify_csrf(); + + $comment = ORM::factory("comment", $id); + $orig = clone $comment; + if ($comment->loaded()) { + $comment->state = $state; + $comment->save(); + } + } + + public function delete_all_spam() { + access::verify_csrf(); + + db::build() + ->delete("comments") + ->where("state", "=", "spam") + ->execute(); + url::redirect("admin/manage_comments/queue/spam"); + } +} + diff --git a/modules/comment/controllers/comments.php b/modules/comment/controllers/comments.php new file mode 100644 index 0000000..64aa0b4 --- /dev/null +++ b/modules/comment/controllers/comments.php @@ -0,0 +1,81 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Comments_Controller extends Controller { + /** + * Add a new comment to the collection. + */ + public function create($id) { + $item = ORM::factory("item", $id); + access::required("view", $item); + if (!comment::can_comment()) { + access::forbidden(); + } + + $form = comment::get_add_form($item); + try { + $valid = $form->validate(); + $comment = ORM::factory("comment"); + $comment->item_id = $id; + $comment->author_id = identity::active_user()->id; + $comment->text = $form->add_comment->text->value; + $comment->guest_name = $form->add_comment->inputs["name"]->value; + $comment->guest_email = $form->add_comment->email->value; + $comment->guest_url = $form->add_comment->url->value; + $comment->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + switch ($key) { + case "guest_name": $key = "name"; break; + case "guest_email": $key = "email"; break; + case "guest_url": $key = "url"; break; + } + $form->add_comment->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $comment->save(); + $view = new Theme_View("comment.html", "other", "comment-fragment"); + $view->comment = $comment; + + json::reply(array("result" => "success", + "view" => (string)$view, + "form" => (string)comment::get_add_form($item))); + } else { + $form = comment::prefill_add_form($form); + json::reply(array("result" => "error", "form" => (string)$form)); + } + } + + /** + * Present a form for adding a new comment to this item or editing an existing comment. + */ + public function form_add($item_id) { + $item = ORM::factory("item", $item_id); + access::required("view", $item); + if (!comment::can_comment()) { + access::forbidden(); + } + + print comment::prefill_add_form(comment::get_add_form($item)); + } +} diff --git a/modules/comment/css/comment.css b/modules/comment/css/comment.css new file mode 100644 index 0000000..db096f2 --- /dev/null +++ b/modules/comment/css/comment.css @@ -0,0 +1,45 @@ +#g-content #g-comment-form { + margin-top: 2em; +} + +#g-content #g-comments { + margin-top: 2em; + position: relative; +} + +#g-content #g-comments ul li { + margin: 1em 0; +} + +#g-content #g-comments .g-author { + border-bottom: 1px solid #ccc; + color: #999; + height: 32px; + line-height: 32px; +} + +#g-content #g-comments ul li div { + padding: 0 8px 8px 43px; +} + +#g-content #g-comments .g-avatar { + height: 32px; + margin-right: .4em; + width: 32px; +} + +#g-add-comment { + position: absolute; + right: 0; + top: 2px; +} + +#g-admin-comments-menu { + margin: 1em 0; +} + +#g-admin-comments-menu a { + margin: 0; + padding: .2em .6em; +} + diff --git a/modules/comment/helpers/comment.php b/modules/comment/helpers/comment.php new file mode 100644 index 0000000..0d922eb --- /dev/null +++ b/modules/comment/helpers/comment.php @@ -0,0 +1,71 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling comments. + * + * Note: by design, this class does not do any permission checking. + */ +class comment_Core { + static function get_add_form($item) { + $form = new Forge("comments/create/{$item->id}", "", "post", array("id" => "g-comment-form")); + $group = $form->group("add_comment")->label(t("Add comment")); + $group->input("name") + ->label(t("Name")) + ->id("g-author") + ->error_messages("required", t("You must enter a name for yourself")); + $group->input("email") + ->label(t("Email (hidden)")) + ->id("g-email") + ->error_messages("required", t("You must enter a valid email address")) + ->error_messages("invalid", t("You must enter a valid email address")); + $group->input("url") + ->label(t("Website (hidden)")) + ->id("g-url") + ->error_messages("url", t("You must enter a valid url")); + $group->textarea("text") + ->label(t("Comment")) + ->id("g-text") + ->error_messages("required", t("You must enter a comment")); + $group->hidden("item_id")->value($item->id); + module::event("comment_add_form", $form); + module::event("captcha_protect_form", $form); + $group->submit("")->value(t("Add"))->class("ui-state-default ui-corner-all"); + + return $form; + } + + static function prefill_add_form($form) { + $active = identity::active_user(); + if (!$active->guest) { + $group = $form->add_comment; + $group->inputs["name"]->value($active->full_name)->disabled("disabled"); + $group->email->value($active->email)->disabled("disabled"); + $group->url->value($active->url)->disabled("disabled"); + } + return $form; + } + + static function can_comment() { + return !identity::active_user()->guest || + module::get_var("comment", "access_permissions") == "everybody"; + } +} + diff --git a/modules/comment/helpers/comment_block.php b/modules/comment/helpers/comment_block.php new file mode 100644 index 0000000..b602595 --- /dev/null +++ b/modules/comment/helpers/comment_block.php @@ -0,0 +1,39 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comment_block_Core { + static function get_admin_list() { + return array("recent_comments" => t("Recent comments")); + } + + static function get($block_id) { + $block = new Block(); + switch ($block_id) { + case "recent_comments": + $block->css_id = "g-recent-comments"; + $block->title = t("Recent comments"); + $block->content = new View("admin_block_recent_comments.html"); + $block->content->comments = + ORM::factory("comment")->order_by("created", "DESC")->limit(5)->find_all(); + break; + } + + return $block; + } +}
\ No newline at end of file diff --git a/modules/comment/helpers/comment_event.php b/modules/comment/helpers/comment_event.php new file mode 100644 index 0000000..f73e545 --- /dev/null +++ b/modules/comment/helpers/comment_event.php @@ -0,0 +1,97 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comment_event_Core { + static function item_deleted($item) { + db::build() + ->delete("comments") + ->where("item_id", "=", $item->id) + ->execute(); + } + + static function user_deleted($user) { + $guest = identity::guest(); + if (!empty($guest)) { // could be empty if there is not identity provider + db::build() + ->update("comments") + ->set("author_id", $guest->id) + ->set("guest_email", null) + ->set("guest_name", "guest") + ->set("guest_url", null) + ->where("author_id", "=", $user->id) + ->execute(); + } + } + + static function identity_provider_changed($old_provider, $new_provider) { + $guest = identity::guest(); + db::build() + ->update("comments") + ->set("author_id", $guest->id) + ->set("guest_email", null) + ->set("guest_name", "guest") + ->set("guest_url", null) + ->execute(); + } + + static function admin_menu($menu, $theme) { + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("comment") + ->label(t("Comments")) + ->url(url::site("admin/comments"))); + + $menu->get("content_menu") + ->append(Menu::factory("link") + ->id("comments") + ->label(t("Comments")) + ->url(url::site("admin/manage_comments"))); + } + + static function photo_menu($menu, $theme) { + $menu + ->append(Menu::factory("link") + ->id("comments") + ->label(t("View comments on this item")) + ->url("#comments") + ->css_id("g-comments-link")); + } + + static function item_index_data($item, $data) { + foreach (db::build() + ->select("text") + ->from("comments") + ->where("item_id", "=", $item->id) + ->execute() as $row) { + $data[] = $row->text; + } + } + + static function show_user_profile($data) { + $view = new View("user_profile_comments.html"); + $view->comments = ORM::factory("comment") + ->order_by("created", "DESC") + ->where("state", "=", "published") + ->where("author_id", "=", $data->user->id) + ->find_all(); + if ($view->comments->count()) { + $data->content[] = (object)array("title" => t("Comments"), "view" => $view); + } + } +} diff --git a/modules/comment/helpers/comment_installer.php b/modules/comment/helpers/comment_installer.php new file mode 100644 index 0000000..136f96e --- /dev/null +++ b/modules/comment/helpers/comment_installer.php @@ -0,0 +1,118 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comment_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {comments} ( + `author_id` int(9) default NULL, + `created` int(9) NOT NULL, + `guest_email` varchar(128) default NULL, + `guest_name` varchar(128) default NULL, + `guest_url` varchar(255) default NULL, + `id` int(9) NOT NULL auto_increment, + `item_id` int(9) NOT NULL, + `server_http_accept_charset` varchar(64) default NULL, + `server_http_accept_encoding` varchar(64) default NULL, + `server_http_accept_language` varchar(64) default NULL, + `server_http_accept` varchar(128) default NULL, + `server_http_connection` varchar(64) default NULL, + `server_http_host` varchar(64) default NULL, + `server_http_referer` varchar(255) default NULL, + `server_http_user_agent` varchar(128) default NULL, + `server_query_string` varchar(64) default NULL, + `server_remote_addr` varchar(40) default NULL, + `server_remote_host` varchar(255) default NULL, + `server_remote_port` varchar(16) default NULL, + `state` varchar(15) default 'unpublished', + `text` text, + `updated` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + + module::set_var("comment", "spam_caught", 0); + module::set_var("comment", "access_permissions", "everybody"); + module::set_var("comment", "rss_visible", "all"); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + $db->query("ALTER TABLE {comments} CHANGE `state` `state` varchar(15) default 'unpublished'"); + module::set_version("comment", $version = 2); + } + + if ($version == 2) { + module::set_var("comment", "access_permissions", "everybody"); + module::set_version("comment", $version = 3); + } + + if ($version == 3) { + // 40 bytes for server_remote_addr is enough to swallow the longest + // representation of an IPv6 addy. + // + // 255 bytes for server_remote_host is enough to swallow the longest + // legit DNS entry, with a few bytes to spare. + $db->query( + "ALTER TABLE {comments} CHANGE `server_remote_addr` `server_remote_addr` varchar(40)"); + $db->query( + "ALTER TABLE {comments} CHANGE `server_remote_host` `server_remote_host` varchar(255)"); + module::set_version("comment", $version = 4); + } + + if ($version == 4) { + module::set_var("comment", "rss_visible", "all"); + module::set_version("comment", $version = 5); + } + + // In version 5 we accidentally set the installer variable to rss_available when it should + // have been rss_visible. Migrate it over now, if necessary. + if ($version == 5) { + if (!module::get_var("comment", "rss_visible")) { + module::set_var("comment", "rss_visible", module::get_var("comment", "rss_available")); + } + module::clear_var("comment", "rss_available"); + module::set_version("comment", $version = 6); + } + + // In version 6 we accidentally left the install value of "rss_visible" to "both" when it + // should have been "all" + if ($version == 6) { + if (module::get_var("comment", "rss_visible") == "both") { + module::set_var("comment", "rss_visible", "all"); + } + module::set_version("comment", $version = 7); + } + } + + static function uninstall() { + $db = Database::instance(); + + // Notify listeners that we're deleting some data. This is probably going to be very + // inefficient for large uninstalls, and we could make it better by doing things like passing + // a SQL fragment through so that the listeners could use subselects. But by using a single, + // simple event API we lighten the load on module developers. + foreach (ORM::factory("item") + ->join("comments", "items.id", "comments.item_id") + ->find_all() as $item) { + module::event("item_related_update", $item); + } + $db->query("DROP TABLE IF EXISTS {comments};"); + } +} diff --git a/modules/comment/helpers/comment_rest.php b/modules/comment/helpers/comment_rest.php new file mode 100644 index 0000000..1971edc --- /dev/null +++ b/modules/comment/helpers/comment_rest.php @@ -0,0 +1,74 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comment_rest_Core { + static function get($request) { + $comment = rest::resolve($request->url); + access::required("view", $comment->item()); + + return array( + "url" => $request->url, + "entity" => $comment->as_restful_array(), + "relationships" => rest::relationships("comment", $comment)); + } + + static function put($request) { + // Only admins can edit comments, for now + if (!identity::active_user()->admin) { + access::forbidden(); + } + + $comment = rest::resolve($request->url); + $comment = ORM::factory("comment"); + $comment->text = $request->params->text; + $comment->save(); + } + + static function delete($request) { + if (!identity::active_user()->admin) { + access::forbidden(); + } + + $comment = rest::resolve($request->url); + access::required("edit", $comment->item()); + + $comment->delete(); + } + + static function relationships($resource_type, $resource) { + switch ($resource_type) { + case "item": + return array( + "comments" => array( + "url" => rest::url("item_comments", $resource))); + } + } + + static function resolve($id) { + $comment = ORM::factory("comment", $id); + if (!access::can("view", $comment->item())) { + throw new Kohana_404_Exception(); + } + return $comment; + } + + static function url($comment) { + return url::abs_site("rest/comment/{$comment->id}"); + } +} diff --git a/modules/comment/helpers/comment_rss.php b/modules/comment/helpers/comment_rss.php new file mode 100644 index 0000000..924710f --- /dev/null +++ b/modules/comment/helpers/comment_rss.php @@ -0,0 +1,92 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class comment_rss_Core { + static function feed_visible($feed_id) { + $visible = module::get_var("comment", "rss_visible"); + if (!in_array($feed_id, array("newest", "per_item"))) { + return false; + } + + return ($visible == "all" || $visible == $feed_id); + } + + static function available_feeds($item, $tag) { + $feeds = array(); + + if (comment_rss::feed_visible("newest")) { + $feeds["comment/newest"] = t("All new comments"); + } + + if ($item && comment_rss::feed_visible("per_item")) { + $feeds["comment/per_item/$item->id"] = + t("Comments on %title", array("title" => html::purify($item->title))); + } + return $feeds; + } + + static function feed($feed_id, $offset, $limit, $id) { + if (!comment_rss::feed_visible($feed_id)) { + return; + } + + $comments = ORM::factory("comment") + ->viewable() + ->where("comments.state", "=", "published") + ->order_by("comments.created", "DESC"); + + if ($feed_id == "item") { + $item = ORM::factory("item", $id); + $comments + ->where("items.left_ptr", ">=", $item->left_ptr) + ->where("items.right_ptr", "<=", $item->right_ptr); + } + + $feed = new stdClass(); + $feed->view = "comment.mrss"; + $feed->comments = array(); + foreach ($comments->find_all($limit, $offset) as $comment) { + $item = $comment->item(); + $feed->comments[] = new ArrayObject( + array("pub_date" => date("D, d M Y H:i:s O", $comment->created), + "text" => nl2br(html::purify($comment->text)), + "thumb_url" => $item->thumb_url(), + "thumb_height" => $item->thumb_height, + "thumb_width" => $item->thumb_width, + "item_uri" => url::abs_site("{$item->type}s/$item->id"), + "title" => ( + ($item->id == item::root()->id) ? + html::purify($item->title) : + t("%site_title - %item_title", + array("site_title" => item::root()->title, + "item_title" => $item->title))), + "author" => html::clean($comment->author_name())), + ArrayObject::ARRAY_AS_PROPS); + } + + $feed->max_pages = ceil($comments->count_all() / $limit); + $feed->title = html::purify(t("%site_title - Recent Comments", + array("site_title" => item::root()->title))); + $feed->uri = url::abs_site("albums/" . (empty($id) ? "1" : $id)); + $feed->description = t("Recent comments"); + + return $feed; + } +} diff --git a/modules/comment/helpers/comment_theme.php b/modules/comment/helpers/comment_theme.php new file mode 100644 index 0000000..1c2d7c5 --- /dev/null +++ b/modules/comment/helpers/comment_theme.php @@ -0,0 +1,46 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comment_theme_Core { + static function head($theme) { + return $theme->css("comment.css") + . $theme->script("comment.js"); + } + + static function admin_head($theme) { + return $theme->css("comment.css"); + } + + static function photo_bottom($theme) { + $block = new Block; + $block->css_id = "g-comments"; + $block->title = t("Comments"); + $block->anchor = "comments"; + + $view = new View("comments.html"); + $view->comments = ORM::factory("comment") + ->where("item_id", "=", $theme->item()->id) + ->where("state", "=", "published") + ->order_by("created", "ASC") + ->find_all(); + + $block->content = $view; + return $block; + } +}
\ No newline at end of file diff --git a/modules/comment/helpers/comments_rest.php b/modules/comment/helpers/comments_rest.php new file mode 100644 index 0000000..6fc86ad --- /dev/null +++ b/modules/comment/helpers/comments_rest.php @@ -0,0 +1,62 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class comments_rest_Core { + /** + * Possible request parameters: + * start=# + * start at the Nth comment (zero based) + * + * num=# + * return up to N comments (max 100) + */ + static function get($request) { + $comments = array(); + + $p = $request->params; + $num = isset($p->num) ? min((int)$p->num, 100) : 10; + $start = isset($p->start) ? (int)$p->start : 0; + + foreach (ORM::factory("comment")->viewable()->find_all($num, $start) as $comment) { + $comments[] = rest::url("comment", $comment); + } + return array("url" => rest::url("comments"), + "members" => $comments); + } + + + static function post($request) { + $entity = $request->params->entity; + + $item = rest::resolve($entity->item); + access::required("edit", $item); + + $comment = ORM::factory("comment"); + $comment->author_id = identity::active_user()->id; + $comment->item_id = $item->id; + $comment->text = $entity->text; + $comment->save(); + + return array("url" => rest::url("comment", $comment)); + } + + static function url() { + return url::abs_site("rest/comments"); + } +} diff --git a/modules/comment/helpers/item_comments_rest.php b/modules/comment/helpers/item_comments_rest.php new file mode 100644 index 0000000..f6f8930 --- /dev/null +++ b/modules/comment/helpers/item_comments_rest.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class item_comments_rest_Core { + static function get($request) { + $item = rest::resolve($request->url); + access::required("view", $item); + + $comments = array(); + foreach (ORM::factory("comment") + ->viewable() + ->where("item_id", "=", $item->id) + ->order_by("created", "DESC") + ->find_all() as $comment) { + $comments[] = rest::url("comment", $comment); + } + + return array( + "url" => $request->url, + "members" => $comments); + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + return $item; + } + + static function url($item) { + return url::abs_site("rest/item_comments/{$item->id}"); + } +} diff --git a/modules/comment/js/comment.js b/modules/comment/js/comment.js new file mode 100644 index 0000000..2487c5f --- /dev/null +++ b/modules/comment/js/comment.js @@ -0,0 +1,45 @@ +$("document").ready(function() { + $("#g-add-comment").click(function(event) { + event.preventDefault(); + if (!$("#g-comment-form").length) { + $.get($(this).attr("href"), + {}, + function(data) { + $("#g-comment-detail").append(data); + ajaxify_comment_form(); + $.scrollTo("#g-comment-form-anchor", 800); + }); + } + }); + $(".g-no-comments a").click(function(event) { + event.preventDefault(); + if (!$("#g-comment-form").length) { + $.get($(this).attr("href"), + {}, + function(data) { + $("#g-comment-detail").append(data); + ajaxify_comment_form(); + }); + $(".g-no-comments").remove(); + } + }); +}); + +function ajaxify_comment_form() { + $("#g-comments form").ajaxForm({ + dataType: "json", + success: function(data) { + if (data.result == "success") { + $("#g-comments #g-comment-detail ul").append(data.view); + $("#g-comments #g-comment-detail ul li:last").effect("highlight", {color: "#cfc"}, 8000); + $("#g-comment-form").hide(2000).remove(); + $("#g-no-comments").hide(2000); + } else { + if (data.form) { + $("#g-comments form").replaceWith(data.form); + ajaxify_comment_form(); + } + } + } + }); +} diff --git a/modules/comment/models/comment.php b/modules/comment/models/comment.php new file mode 100644 index 0000000..62ab8bc --- /dev/null +++ b/modules/comment/models/comment.php @@ -0,0 +1,194 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Comment_Model_Core extends ORM { + function item() { + return ORM::factory("item", $this->item_id); + } + + function author() { + return identity::lookup_user($this->author_id); + } + + function author_name() { + $author = $this->author(); + if ($author->guest) { + return $this->guest_name; + } else { + return $author->display_name(); + } + } + + function author_email() { + $author = $this->author(); + if ($author->guest) { + return $this->guest_email; + } else { + return $author->email; + } + } + + function author_url() { + $author = $this->author(); + if ($author->guest) { + return $this->guest_url; + } else { + return $author->url; + } + } + + /** + * Add some custom per-instance rules. + */ + public function validate(Validation $array=null) { + // validate() is recursive, only modify the rules on the outermost call. + if (!$array) { + $this->rules = array( + "guest_name" => array("callbacks" => array(array($this, "valid_author"))), + "guest_email" => array("callbacks" => array(array($this, "valid_email"))), + "guest_url" => array("rules" => array("url")), + "item_id" => array("callbacks" => array(array($this, "valid_item"))), + "state" => array("rules" => array("Comment_Model::valid_state")), + "text" => array("rules" => array("required")), + ); + } + + parent::validate($array); + } + + /** + * @see ORM::save() + */ + public function save() { + $this->updated = time(); + if (!$this->loaded()) { + // New comment + $this->created = $this->updated; + if (empty($this->state)) { + $this->state = "published"; + } + + // These values are useful for spam fighting, so save them with the comment. It's painful to + // check each one to see if it already exists before setting it, so just use server_http_host + // as a semaphore for now (we use that in g2_import.php) + if (empty($this->server_http_host)) { + $input = Input::instance(); + $this->server_http_accept = substr($input->server("HTTP_ACCEPT"), 0, 128); + $this->server_http_accept_charset = substr($input->server("HTTP_ACCEPT_CHARSET"), 0, 64); + $this->server_http_accept_encoding = substr($input->server("HTTP_ACCEPT_ENCODING"), 0, 64); + $this->server_http_accept_language = substr($input->server("HTTP_ACCEPT_LANGUAGE"), 0, 64); + $this->server_http_connection = substr($input->server("HTTP_CONNECTION"), 0, 64); + $this->server_http_host = substr($input->server("HTTP_HOST"), 0, 64); + $this->server_http_referer = substr($input->server("HTTP_REFERER"), 0, 255); + $this->server_http_user_agent = substr($input->server("HTTP_USER_AGENT"), 0, 128); + $this->server_query_string = substr($input->server("QUERY_STRING"), 0, 64); + $this->server_remote_addr = substr($input->server("REMOTE_ADDR"), 0, 40); + $this->server_remote_host = substr($input->server("REMOTE_HOST"), 0, 255); + $this->server_remote_port = substr($input->server("REMOTE_PORT"), 0, 16); + } + + $visible_change = $this->state == "published"; + parent::save(); + module::event("comment_created", $this); + } else { + // Updated comment + $original = ORM::factory("comment", $this->id); + $visible_change = $original->state == "published" || $this->state == "published"; + parent::save(); + module::event("comment_updated", $original, $this); + } + + // We only notify on the related items if we're making a visible change. + if ($visible_change) { + $item = $this->item(); + module::event("item_related_update", $item); + } + + return $this; + } + + /** + * Add a set of restrictions to any following queries to restrict access only to items + * viewable by the active user. + * @chainable + */ + public function viewable() { + $this->join("items", "items.id", "comments.item_id"); + return item::viewable($this); + } + + /** + * Make sure we have an appropriate author id set, or a guest name. + */ + public function valid_author(Validation $v, $field) { + if (empty($this->author_id)) { + $v->add_error("author_id", "required"); + } else if ($this->author_id == identity::guest()->id && empty($this->guest_name)) { + $v->add_error("guest_name", "required"); + } + } + + /** + * Make sure that the email address is legal. + */ + public function valid_email(Validation $v, $field) { + if ($this->author_id == identity::guest()->id) { + if (empty($v->guest_email)) { + $v->add_error("guest_email", "required"); + } else if (!valid::email($v->guest_email)) { + $v->add_error("guest_email", "invalid"); + } + } + } + + /** + * Make sure we have a valid associated item id. + */ + public function valid_item(Validation $v, $field) { + if (db::build() + ->from("items") + ->where("id", "=", $this->item_id) + ->count_records() != 1) { + $v->add_error("item_id", "invalid"); + } + } + + /** + * Make sure that the state is legal. + */ + static function valid_state($value) { + return in_array($value, array("published", "unpublished", "spam", "deleted")); + } + + /** + * Same as ORM::as_array() but convert id fields into their RESTful form. + */ + public function as_restful_array() { + $data = array(); + foreach ($this->as_array() as $key => $value) { + if (strncmp($key, "server_", 7)) { + $data[$key] = $value; + } + } + $data["item"] = rest::url("item", $this->item()); + unset($data["item_id"]); + + return $data; + } +} diff --git a/modules/comment/module.info b/modules/comment/module.info new file mode 100644 index 0000000..b69379f --- /dev/null +++ b/modules/comment/module.info @@ -0,0 +1,7 @@ +name = "Comments" +description = "Allows users and guests to leave comments on photos and albums." +version = 7 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:comment" +discuss_url = "http://galleryproject.org/forum_module_comment" diff --git a/modules/comment/views/admin_block_recent_comments.html.php b/modules/comment/views/admin_block_recent_comments.html.php new file mode 100644 index 0000000..4017e4f --- /dev/null +++ b/modules/comment/views/admin_block_recent_comments.html.php @@ -0,0 +1,23 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <? foreach ($comments as $comment): ?> + <li class="<?= text::alternate("g-even", "g-odd") ?>"> + <img src="<?= $comment->author()->avatar_url(32, $theme->url("images/avatar.jpg", true)) ?>" + class="g-avatar" + alt="<?= html::clean_attribute($comment->author_name()) ?>" + width="32" + height="32" /> + <?= gallery::date_time($comment->created) ?> + <? if ($comment->author()->guest): ?> + <?= t('%author_name said <em>%comment_text</em>', + array("author_name" => html::clean($comment->author_name()), + "comment_text" => text::limit_words(nl2br(html::purify($comment->text)), 50))); ?> + <? else: ?> + <?= t('<a href="%url">%author_name</a> said <em>%comment_text</em>', + array("author_name" => html::clean($comment->author_name()), + "url" => user_profile::url($comment->author_id), + "comment_text" => text::limit_words(nl2br(html::purify($comment->text)), 50))); ?> + <? endif ?> + </li> + <? endforeach ?> +</ul> diff --git a/modules/comment/views/admin_comments.html.php b/modules/comment/views/admin_comments.html.php new file mode 100644 index 0000000..dc6985b --- /dev/null +++ b/modules/comment/views/admin_comments.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block"> + <h1> <?= t("Comment settings") ?> </h1> + <div class="g-block-content"> + <?= $form ?> + </div> +</div> diff --git a/modules/comment/views/admin_manage_comments.html.php b/modules/comment/views/admin_manage_comments.html.php new file mode 100644 index 0000000..e3c8546 --- /dev/null +++ b/modules/comment/views/admin_manage_comments.html.php @@ -0,0 +1,46 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var set_state_url = + <?= html::js_string(url::site("admin/manage_comments/set_state/__ID__/__STATE__?csrf=$csrf")) ?>; + var set_state = function(state, id) { + $("#g-comment-" + id).fadeOut("fast", function() { + $.get(set_state_url.replace("__STATE__", state).replace("__ID__", id), + {}, + update_menu); + }); + } + + var update_menu = function() { + $.get(<?= html::js_string(url::site("admin/manage_comments/menu_labels")) ?>, {}, + function(data) { + for (var i = 0; i < data.length; i++) { + $("#g-admin-comments ul li:eq(" + i + ") a").html(data[i]); + } + }, + "json"); + } + + // Paginator clicks load their href in the active tab panel + var fix_links = function() { + $(".g-paginator a, a#g-delete-all-spam").click(function(event) { + event.stopPropagation(); + $.scrollTo(0, 800, { easing: "swing" }); + $(this).parents(".ui-tabs-panel").load( + $(this).attr("href"), + function() { + fix_links(); + }); + return false; + }); + } + + $(document).ready(function() { + $("#g-admin-comments").tabs({ + show: fix_links, + }); + }); +</script> + +<div id="g-admin-comments" class="g-block"> + <?= $menu->render() ?> +</div> diff --git a/modules/comment/views/admin_manage_comments_queue.html.php b/modules/comment/views/admin_manage_comments_queue.html.php new file mode 100644 index 0000000..d847d72 --- /dev/null +++ b/modules/comment/views/admin_manage_comments_queue.html.php @@ -0,0 +1,157 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block-content"> + <? if ($state == "spam"): ?> + <div> + <? $spam_caught = module::get_var("comment", "spam_caught") ?> + <? if ($spam_caught > 0): ?> + <p> + <?= t2("Gallery has caught %count spam for you since you installed spam filtering.", + "Gallery has caught %count spam for you since you installed spam filtering.", + $spam_caught) ?> + </p> + <? endif ?> + <p> + <? if ($counts->spam): ?> + <?= t2("There is currently one comment in your spam queue. You can delete it with a single click, but there is no undo operation so you may want to check the message first to make sure that it really is spam.", + "There are currently %count comments in your spam queue. You can delete them all with a single click, but there is no undo operation so you may want to check the messages first to make sure that they really are spam. All spam messages will be deleted after 7 days automatically.", + $counts->spam) ?> + </p> + <p> + <a id="g-delete-all-spam" + href="<?= url::site("admin/manage_comments/delete_all_spam?csrf=$csrf") ?>"> + <?= t("Delete all spam") ?> + </a> + <? else: ?> + <?= t("Your spam queue is empty!") ?> + <? endif ?> + </p> + </div> + <? endif ?> + + <? if ($state == "deleted"): ?> + <div> + <p> + <?= t("These are messages that have been recently deleted. They will be permanently erased automatically after 7 days.") ?> + </p> + </div> + <? endif ?> + + <div class="g-paginator"> + <?= $theme->paginator() ?> + </div> + <table id="g-admin-comments-list"> + <tr> + <th> + <?= t("Author") ?> + </th> + <th> + <?= t("Comment") ?> + </th> + <th> + <?= t("Actions") ?> + </th> + </tr> + <? foreach ($comments as $comment): ?> + <tr id="g-comment-<?= $comment->id ?>" class="<?= text::alternate("g-odd", "g-even") ?>"> + <td> + <a href="#"> + <img src="<?= $comment->author()->avatar_url(40, $fallback_avatar_url) ?>" + class="g-avatar" + alt="<?= html::clean_attribute($comment->author_name()) ?>" + width="40" + height="40" /> + </a> + <p> + <a href="mailto:<?= html::clean_attribute($comment->author_email()) ?>" + title="<?= html::clean_attribute($comment->author_email()) ?>"> + <?= html::clean($comment->author_name()) ?> + </a> + </p> + </td> + <td> + <div class="g-right"> + <? $item = $comment->item() ?> + <div class="g-item g-photo"> + <a href="<?= $item->url() ?>"> + <? if ($item->has_thumb()): ?> + <img src="<?= $item->thumb_url() ?>" + alt="<?= html::purify($item->title)->for_html_attr() ?>" + <?= photo::img_dimensions($item->thumb_width, $item->thumb_height, 75) ?> + /> + <? else: ?> + <?= t("No thumbnail") ?> + <? endif ?> + </a> + </div> + </div> + <p><?= gallery::date($comment->created) ?></p> + <?= nl2br(html::purify($comment->text)) ?> + </td> + <td> + <ul class="g-buttonset-vertical"> + <? if ($comment->state != "unpublished" && $comment->state != "deleted"): ?> + <li> + <a href="javascript:set_state('unpublished',<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-check"></span> + <?= t("Unapprove") ?> + </a> + </li> + <? endif ?> + <? if ($comment->state != "published"): ?> + <li> + <a href="javascript:set_state('published',<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-check"></span> + <? if ($state == "deleted"): ?> + <?= t("Undelete") ?> + <? else: ?> + <?= t("Approve") ?> + <? endif ?> + </a> + </li> + <? endif ?> + <? if ($comment->state != "spam"): ?> + <li> + <a href="javascript:set_state('spam',<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-cancel"></span> + <?= t("Spam") ?> + </a> + </li> + <? endif ?> + <!-- + <li> + <a href="javascript:reply(<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-arrowreturnthick-1-w"></span> + <?= t("Reply") ?> + </a> + </li> + <li> + <a href="javascript:Edit(<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-pencil"></span> + <?= t("Edit") ?> + </a> + </li> + --> + <? if ($comment->state != "deleted"): ?> + <li> + <a href="javascript:set_state('deleted',<?=$comment->id?>)" + class="g-button ui-state-default ui-icon-left"> + <span class="ui-icon ui-icon-trash"></span> + <?= t("Delete") ?> + </a> + </li> + <? endif ?> + </ul> + </td> + </tr> + <? endforeach ?> + </table> + + <div class="g-paginator"> + <?= $theme->paginator() ?> + </div> +</div> diff --git a/modules/comment/views/comment.html.php b/modules/comment/views/comment.html.php new file mode 100644 index 0000000..263e5f9 --- /dev/null +++ b/modules/comment/views/comment.html.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li id="g-comment-<?= $comment->id; ?>"> + <p class="g-author"> + <a href="#"> + <img src="<?= $comment->author()->avatar_url(40, $theme->url("images/avatar.jpg", true)) ?>" + class="g-avatar" + alt="<?= html::clean_attribute($comment->author_name()) ?>" + width="40" + height="40" /> + </a> + <? if ($comment->author()->guest): ?> + <?= t("on %date_time, %name said", + array("date_time" => gallery::date_time($comment->created), + "name" => html::clean($comment->author_name()))) ?> + <? else: ?> + <?= t("on %date_time, <a href=\"%url\">%name</a> said", + array("date_time" => gallery::date_time($comment->created), + "url" => user_profile::url($comment->author_id), + "name" => html::clean($comment->author_name()))) ?> + <? endif ?> + </p> + <div> + <?= nl2br(html::purify($comment->text)) ?> + </div> +</li> diff --git a/modules/comment/views/comment.mrss.php b/modules/comment/views/comment.mrss.php new file mode 100644 index 0000000..809e789 --- /dev/null +++ b/modules/comment/views/comment.mrss.php @@ -0,0 +1,43 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? echo "<?xml version=\"1.0\" ?>" ?> +<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:content="http://purl.org/rss/1.0/modules/content/" + xmlns:fh="http://purl.org/syndication/history/1.0"> + <channel> + <generator>Gallery 3</generator> + <title><?= html::clean($feed->title) ?></title> + <link><?= $feed->uri ?></link> + <description><?= html::clean($feed->description) ?></description> + <language>en-us</language> + <atom:link rel="self" href="<?= $feed->uri ?>" type="application/rss+xml" /> + <fh:complete/> + <? if (!empty($feed->previous_page_uri)): ?> + <atom:link rel="previous" href="<?= $feed->previous_page_uri ?>" type="application/rss+xml" /> + <? endif ?> + <? if (!empty($feed->next_page_uri)): ?> + <atom:link rel="next" href="<?= $feed->next_page_uri ?>" type="application/rss+xml" /> + <? endif ?> + <pubDate><?= $pub_date ?></pubDate> + <lastBuildDate><?= $pub_date ?></lastBuildDate> + <? foreach ($feed->comments as $comment): ?> + <item> + <title><?= html::purify($comment->title) ?></title> + <link><?= html::clean($comment->item_uri) ?></link> + <author><?= html::clean($comment->author) ?></author> + <guid isPermaLink="true"><?= $comment->item_uri ?></guid> + <pubDate><?= $comment->pub_date ?></pubDate> + <content:encoded> + <![CDATA[ + <p><?= nl2br(html::purify($comment->text)) ?></p> + <p> + <img alt="" src="<?= $comment->thumb_url ?>" + height="<?= $comment->thumb_height ?>" width="<?= $comment->thumb_width ?>" /> + <br /> + </p> + ]]> + </content:encoded> + </item> + <? endforeach ?> + </channel> +</rss> diff --git a/modules/comment/views/comments.html.php b/modules/comment/views/comments.html.php new file mode 100644 index 0000000..b524f5d --- /dev/null +++ b/modules/comment/views/comments.html.php @@ -0,0 +1,56 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? if (comment::can_comment()): ?> +<a href="<?= url::site("form/add/comments/{$item->id}") ?>#comment-form" id="g-add-comment" + class="g-button ui-corner-all ui-icon-left ui-state-default"> + <span class="ui-icon ui-icon-comment"></span> + <?= t("Add a comment") ?> +</a> +<? endif ?> + +<div id="g-comment-detail"> + <? if (!$comments->count()): ?> + <p class="g-no-comments"> + <? if (comment::can_comment()): ?> + <?= t("No comments yet. Be the first to <a %attrs>comment</a>!", + array("attrs" => html::mark_clean("href=\"" . url::site("form/add/comments/{$item->id}") . "\" class=\"showCommentForm\""))) ?> + <? else: ?> + <?= t("No comments yet.") ?> + <? endif ?> + </p> + <ul> + <li class="g-no-comments"> </li> + </ul> + <? endif ?> + + <? if ($comments->count()): ?> + <ul> + <? foreach ($comments as $comment): ?> + <li id="g-comment-<?= $comment->id ?>"> + <p class="g-author"> + <a href="#"> + <img src="<?= $comment->author()->avatar_url(40, $theme->url("images/avatar.jpg", true)) ?>" + class="g-avatar" + alt="<?= html::clean_attribute($comment->author_name()) ?>" + width="40" + height="40" /> + </a> + <? if ($comment->author()->guest): ?> + <?= t('on %date %name said', + array("date" => gallery::date_time($comment->created), + "name" => html::clean($comment->author_name()))); ?> + <? else: ?> + <?= t('on %date <a href="%url">%name</a> said', + array("date" => gallery::date_time($comment->created), + "url" => user_profile::url($comment->author_id), + "name" => html::clean($comment->author_name()))); ?> + <? endif ?> + </p> + <div> + <?= nl2br(html::purify($comment->text)) ?> + </div> + </li> + <? endforeach ?> + </ul> + <? endif ?> + <a name="comment-form" id="g-comment-form-anchor"></a> +</div> diff --git a/modules/comment/views/user_profile_comments.html.php b/modules/comment/views/user_profile_comments.html.php new file mode 100644 index 0000000..377b2d9 --- /dev/null +++ b/modules/comment/views/user_profile_comments.html.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-comment-detail"> +<ul> + <? foreach ($comments as $comment): ?> + <li id="g-comment-<?= $comment->id ?>"> + <p class="g-author"> + <?= t("on %date for %title ", + array("date" => gallery::date_time($comment->created), + "title" => $comment->item()->title)); ?> + <a href="<?= $comment->item()->url() ?>"> + <?= $comment->item()->thumb_img(array(), 50) ?> + </a> + </p> + <div> + <?= nl2br(html::purify($comment->text)) ?> + </div> + </li> + <? endforeach ?> +</ul> +</div> diff --git a/modules/downloadalbum/controllers/downloadalbum.php b/modules/downloadalbum/controllers/downloadalbum.php new file mode 100644 index 0000000..f352763 --- /dev/null +++ b/modules/downloadalbum/controllers/downloadalbum.php @@ -0,0 +1,300 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadalbum_Controller extends Controller { + + /** + * Generate a ZIP on-the-fly. + */ + public function zip($container_type, $id) { + switch($container_type) { + case "album": + $container = ORM::factory("item", $id); + if (!$container->is_album()) { + throw new Kohana_Exception('container is not an album: '.$container->relative_path()); + } + + $zipname = (empty($container->name)) + ? 'Gallery.zip' // @todo purified_version_of($container->title).'.zip' + : $container->name.'.zip'; + break; + + case "tag": + // @todo: if the module is not installed, it crash + $container = ORM::factory("tag", $id); + if (is_null($container->name)) { + throw new Kohana_Exception('container is not a tag: '.$id); + } + + $zipname = $container->name.'.zip'; + break; + + default: + throw new Kohana_Exception('unhandled container type: '.$container_type); + } + + $files = $this->getFilesList($container); + + // Calculate ZIP size (look behind for details) + $zipsize = 22; + foreach($files as $f_name => $f_path) { + $zipsize += 76 + 2*strlen($f_name) + filesize($f_path); + } + + // Send headers + $this->prepareOutput(); + $this->sendHeaders($zipname, $zipsize); + + // Generate and send ZIP file + // http://www.pkware.com/documents/casestudies/APPNOTE.TXT (v6.3.2) + $lfh_offset = 0; + $cds = ''; + $cds_offset = 0; + foreach($files as $f_name => $f_path) { + $f_namelen = strlen($f_name); + $f_size = filesize($f_path); + $f_mtime = $this->unix2dostime(filemtime($f_path)); + $f_crc32 = $this->fixBug45028(hexdec(hash_file('crc32b', $f_path, false))); + + // Local file header + echo pack('VvvvVVVVvva' . $f_namelen, + 0x04034b50, // local file header signature (4 bytes) + 0x0a, // version needed to extract (2 bytes) => 1.0 + 0x0800, // general purpose bit flag (2 bytes) => UTF-8 + 0x00, // compression method (2 bytes) => store + $f_mtime, // last mod file time and date (4 bytes) + $f_crc32, // crc-32 (4 bytes) + $f_size, // compressed size (4 bytes) + $f_size, // uncompressed size (4 bytes) + $f_namelen, // file name length (2 bytes) + 0, // extra field length (2 bytes) + + $f_name // file name (variable size) + // extra field (variable size) => n/a + ); + + // File data + readfile($f_path); + + // Data descriptor (n/a) + + // Central directory structure: File header + $cds .= pack('VvvvvVVVVvvvvvVVa' . $f_namelen, + 0x02014b50, // central file header signature (4 bytes) + 0x031e, // version made by (2 bytes) => v3 / Unix + 0x0a, // version needed to extract (2 bytes) => 1.0 + 0x0800, // general purpose bit flag (2 bytes) => UTF-8 + 0x00, // compression method (2 bytes) => store + $f_mtime, // last mod file time and date (4 bytes) + $f_crc32, // crc-32 (4 bytes) + $f_size, // compressed size (4 bytes) + $f_size, // uncompressed size (4 bytes) + $f_namelen, // file name length (2 bytes) + 0, // extra field length (2 bytes) + 0, // file comment length (2 bytes) + 0, // disk number start (2 bytes) + 0, // internal file attributes (2 bytes) + 0x81b40000, // external file attributes (4 bytes) => chmod 664 + $lfh_offset, // relative offset of local header (4 bytes) + + $f_name // file name (variable size) + // extra field (variable size) => n/a + // file comment (variable size) => n/a + ); + + // Update local file header/central directory structure offset + $cds_offset = $lfh_offset += 30 + $f_namelen + $f_size; + } + + // Archive decryption header (n/a) + // Archive extra data record (n/a) + + // Central directory structure: Digital signature (n/a) + echo $cds; // send Central directory structure + + // Zip64 end of central directory record (n/a) + // Zip64 end of central directory locator (n/a) + + // End of central directory record + $numfile = count($files); + $cds_len = strlen($cds); + echo pack('VvvvvVVv', + 0x06054b50, // end of central dir signature (4 bytes) + 0, // number of this disk (2 bytes) + 0, // number of the disk with the start of + // the central directory (2 bytes) + $numfile, // total number of entries in the + // central directory on this disk (2 bytes) + $numfile, // total number of entries in the + // central directory (2 bytes) + $cds_len, // size of the central directory (4 bytes) + $cds_offset, // offset of start of central directory + // with respect to the + // starting disk number (4 bytes) + 0 // .ZIP file comment length (2 bytes) + // .ZIP file comment (variable size) + ); + } + + + /** + * Return the files that must be included in the archive. + */ + private function getFilesList($container) { + $files = array(); + + if( $container instanceof Item_Model && $container->is_album() ) { + $container_realpath = realpath($container->file_path().'/../'); + + $items = $container->viewable() + ->descendants(null, null, array(array("type", "<>", "album"))); + foreach($items as $i) { + if (!access::can('view_full', $i)) { + continue; + } + + $i_realpath = realpath($i->file_path()); + if (!is_readable($i_realpath)) { + continue; + } + + $i_relative_path = str_replace($container_realpath.DIRECTORY_SEPARATOR, '', $i_realpath); + $i_relative_path = str_replace(DIRECTORY_SEPARATOR, '/', $i_relative_path); + $files[$i_relative_path] = $i_realpath; + } + + } else if( $container instanceof Tag_Model ) { + $items = $container->items(); + foreach($items as $i) { + if (!access::can('view_full', $i)) { + continue; + } + + if( $i->is_album() ) { + foreach($this->getFilesList($i) as $f_name => $f_path) { + $files[$container->name.'/'.$f_name] = $f_path; + } + + } else { + $i_realpath = realpath($i->file_path()); + if (!is_readable($i_realpath)) { + continue; + } + + $i_relative_path = $container->name.'/'.$i->name; + $files[$i_relative_path] = $i_realpath; + } + } + } + + if (count($files) === 0) { + throw new Kohana_Exception('no zippable files in ['.$container->name.']'); + } + + return $files; + } + + + /** + * See system/helpers/download.php + */ + private function prepareOutput() { + // Close output buffers + Kohana::close_buffers(FALSE); + // Clear any output + Event::add('system.display', create_function('', 'Kohana::$output = "";')); + } + + /** + * See system/helpers/download.php + */ + private function sendHeaders($filename, $filesize = null) { + if (!is_null($filesize)) { + header('Content-Length: '.$filesize); + } + + // Retrieve MIME type by extension + $mime = Kohana::config('mimes.'.strtolower(substr(strrchr($filename, '.'), 1))); + $mime = empty($mime) ? 'application/octet-stream' : $mime[0]; + header("Content-Type: $mime"); + header('Content-Transfer-Encoding: binary'); + + // Send headers necessary to invoke a "Save As" dialog + header('Content-Disposition: attachment; filename="'.$filename.'"'); + + // Prevent caching + header('Expires: Thu, 01 Jan 1970 00:00:00 GMT'); + + $pragma = 'no-cache'; + $cachecontrol = 'no-cache, max-age=0'; + + // request::user_agent('browser') seems bugged + if (request::user_agent('browser') === 'Internet Explorer' + || stripos(request::user_agent(), 'msie') !== false + || stripos(request::user_agent(), 'internet explorer') !== false) + { + if (request::protocol() === 'https') { + // See http://support.microsoft.com/kb/323308/en-us + $pragma = 'cache'; + $cachecontrol = 'private'; + + } else if (request::user_agent('version') <= '6.0') { + $pragma = ''; + $cachecontrol = 'must-revalidate, post-check=0, pre-check=0'; + } + } + + header('Pragma: '.$pragma); + header('Cache-Control: '.$cachecontrol); + } + + /** + * @return integer DOS date and time + * @param integer _timestamp Unix timestamp + * @desc returns DOS date and time of the timestamp + */ + private function unix2dostime($timestamp) + { + $timebit = getdate($timestamp); + + if ($timebit['year'] < 1980) { + return (1 << 21 | 1 << 16); + } + + $timebit['year'] -= 1980; + + return ($timebit['year'] << 25 | $timebit['mon'] << 21 | + $timebit['mday'] << 16 | $timebit['hours'] << 11 | + $timebit['minutes'] << 5 | $timebit['seconds'] >> 1); + } + + /** + * See http://bugs.php.net/bug.php?id=45028 + */ + private function fixBug45028($hash) { + $output = $hash; + + if( version_compare(PHP_VERSION, '5.2.7', '<') ) { + $str = str_pad(dechex($hash), 8, '0', STR_PAD_LEFT); + $output = hexdec($str{6}.$str{7}.$str{4}.$str{5}.$str{2}.$str{3}.$str{0}.$str{1}); + } + + return $output; + } +} diff --git a/modules/downloadalbum/css/downloadalbum_menu.css b/modules/downloadalbum/css/downloadalbum_menu.css new file mode 100644 index 0000000..e3c4c67 --- /dev/null +++ b/modules/downloadalbum/css/downloadalbum_menu.css @@ -0,0 +1,3 @@ +#g-view-menu #g-download-album-link { + background-image: url('../images/ico-view-downloadalbum.png'); +} diff --git a/modules/downloadalbum/helpers/downloadalbum_event.php b/modules/downloadalbum/helpers/downloadalbum_event.php new file mode 100644 index 0000000..c5e875c --- /dev/null +++ b/modules/downloadalbum/helpers/downloadalbum_event.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadalbum_event_Core { + static function album_menu($menu, $theme) { + $downloadLink = url::site("downloadalbum/zip/album/{$theme->item->id}"); + $menu + ->append(Menu::factory("link") + ->id("downloadalbum") + ->label(t("Download Album")) + ->url($downloadLink) + ->css_id("g-download-album-link")); + } + + static function tag_menu($menu, $theme) { + $downloadLink = url::site("downloadalbum/zip/tag/{$theme->tag()->id}"); + $menu + ->append(Menu::factory("link") + ->id("downloadalbum") + ->label(t("Download Album")) + ->url($downloadLink) + ->css_id("g-download-album-link")); + } +} diff --git a/modules/downloadalbum/helpers/downloadalbum_theme.php b/modules/downloadalbum/helpers/downloadalbum_theme.php new file mode 100644 index 0000000..800ab3f --- /dev/null +++ b/modules/downloadalbum/helpers/downloadalbum_theme.php @@ -0,0 +1,24 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadalbum_theme { + static function head($theme) { + return $theme->css("downloadalbum_menu.css"); + } +} diff --git a/modules/downloadalbum/images/ico-view-downloadalbum.png b/modules/downloadalbum/images/ico-view-downloadalbum.png Binary files differnew file mode 100644 index 0000000..ce7804d --- /dev/null +++ b/modules/downloadalbum/images/ico-view-downloadalbum.png diff --git a/modules/downloadalbum/module.info b/modules/downloadalbum/module.info new file mode 100644 index 0000000..f547fd4 --- /dev/null +++ b/modules/downloadalbum/module.info @@ -0,0 +1,7 @@ +name = "DownloadAlbum" +description = "Displays a link to download a ZIP archive of the current album." +version = 2 +author_name = "" +author_url = "" +info_url = "http://codex.gallery2.org/Gallery3:Modules:downloadalbum" +discuss_url = "http://gallery.menalto.com/forum_module_downloadalbum" diff --git a/modules/downloadfullsize/controllers/admin_downloadfullsize.php b/modules/downloadfullsize/controllers/admin_downloadfullsize.php new file mode 100644 index 0000000..3befd33 --- /dev/null +++ b/modules/downloadfullsize/controllers/admin_downloadfullsize.php @@ -0,0 +1,93 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class Admin_DownloadFullsize_Controller extends Admin_Controller { + public function index() { + // Generate a new admin page. + $view = new Admin_View("admin.html"); + $view->content = new View("admin_downloadfullsize.html"); + $view->content->downloadlinks_form = $this->_get_admin_form(); + print $view; + } + + public function saveprefs() { + // Prevent Cross Site Request Forgery + access::verify_csrf(); + + // Figure out which boxes where checked + $dlLinks_array = Input::instance()->post("DownloadLinkOptions"); + $fButton = false; + $download_original_button = false; + for ($i = 0; $i < count($dlLinks_array); $i++) { + if ($dlLinks_array[$i] == "fButton") { + $fButton = true; + } + } + + if (module::is_active("keeporiginal")) { + $keeporiginal_array = Input::instance()->post("DownloadOriginalOptions"); + for ($i = 0; $i < count($keeporiginal_array); $i++) { + if ($keeporiginal_array[$i] == "DownloadOriginalImage") { + $download_original_button = true; + } + } + module::set_var("downloadfullsize", "DownloadOriginalImage", $download_original_button); + } + + // Save Settings. + module::set_var("downloadfullsize", "fButton", $fButton); + message::success(t("Your Selection Has Been Saved.")); + + // Load Admin page. + $view = new Admin_View("admin.html"); + $view->content = new View("admin_downloadfullsize.html"); + $view->content->downloadlinks_form = $this->_get_admin_form(); + print $view; + + } + + private function _get_admin_form() { + // Make a new Form. + $form = new Forge("admin/downloadfullsize/saveprefs", "", "post", + array("id" => "g-download-fullsize-adminForm")); + + // Make an array for the different types of download links. + $linkOptions["fButton"] = array(t("Show Floppy Disk Picture Link"), module::get_var("downloadfullsize", "fButton")); + + // Setup a few checkboxes on the form. + $add_links = $form->group("DownloadLinks"); + $add_links->checklist("DownloadLinkOptions") + ->options($linkOptions); + + if (module::is_active("keeporiginal")) { + $KeepOriginalOptions["DownloadOriginalImage"] = array(t("Allow visitors to download the original image when available?"), module::get_var("downloadfullsize", "DownloadOriginalImage")); + $keeporiginal_group = $form->group("KeepOriginalPrefs") + ->label(t("KeepOriginal Preferences")); + $keeporiginal_group->checklist("DownloadOriginalOptions") + ->options($KeepOriginalOptions); + } + + // Add a save button to the form. + $form->submit("SaveLinks")->value(t("Save")); + + // Return the newly generated form. + return $form; + } +}
\ No newline at end of file diff --git a/modules/downloadfullsize/controllers/downloadfullsize.php b/modules/downloadfullsize/controllers/downloadfullsize.php new file mode 100644 index 0000000..9fa43f7 --- /dev/null +++ b/modules/downloadfullsize/controllers/downloadfullsize.php @@ -0,0 +1,37 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadfullsize_Controller extends Controller { + + public function send($id) { + $item = ORM::factory("item", $id); + access::required("view_full", $item); + + if (module::is_active("keeporiginal") && $item->is_photo() && module::get_var("downloadfullsize", "DownloadOriginalImage")) { + $original_image = VARPATH . "original/" . str_replace(VARPATH . "albums/", "", $item->file_path()); + if (file_exists($original_image)) { + download::force($original_image); + } else { + download::force($item->file_path()); + } + } else { + download::force($item->file_path()); + } + } +} diff --git a/modules/downloadfullsize/css/downloadfullsize_menu.css b/modules/downloadfullsize/css/downloadfullsize_menu.css new file mode 100644 index 0000000..a608a66 --- /dev/null +++ b/modules/downloadfullsize/css/downloadfullsize_menu.css @@ -0,0 +1,3 @@ +#g-view-menu #g-download-fullsize-link { + background-image: url('../images/ico-view-downloadfullsize.png'); +} diff --git a/modules/downloadfullsize/helpers/downloadfullsize_block.php b/modules/downloadfullsize/helpers/downloadfullsize_block.php new file mode 100644 index 0000000..f894442 --- /dev/null +++ b/modules/downloadfullsize/helpers/downloadfullsize_block.php @@ -0,0 +1,51 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadfullsize_block_Core { + static function get_site_list() { + return array("downloadfullsize" => t("Download Item")); + } + + static function get($block_id, $theme) { + $item = $theme->item; + switch ($block_id) { + case "downloadfullsize": + + // If Item is movie then... + if ($item && $item->is_movie() && access::can("view_full", $item)) { + $block = new Block(); + $block->css_id = "g-download-fullsize"; + $block->title = t("Download Movie"); + $block->content = new View("downloadfullsize_block.html"); + return $block; + } + + // If Item is photo then... + if ($item && $item->is_photo() && access::can("view_full", $item)) { + $block = new Block(); + $block->css_id = "g-download-fullsize"; + $block->title = t("Download Photo"); + $block->content = new View("downloadfullsize_block.html"); + return $block; + } + } + return ""; + } + +} diff --git a/modules/downloadfullsize/helpers/downloadfullsize_event.php b/modules/downloadfullsize/helpers/downloadfullsize_event.php new file mode 100644 index 0000000..9c4b990 --- /dev/null +++ b/modules/downloadfullsize/helpers/downloadfullsize_event.php @@ -0,0 +1,57 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadfullsize_event_Core { + static function photo_menu($menu, $theme) { + if (access::can("view_full", $theme->item)) { + if (module::get_var("downloadfullsize", "fButton")) { + $downloadLink = url::site("downloadfullsize/send/{$theme->item->id}"); + $menu + ->append(Menu::factory("link") + ->id("downloadfullsize") + ->label(t("Download Fullsize Image")) + ->url($downloadLink) + ->css_id("g-download-fullsize-link")); + } + } + } + + static function movie_menu($menu, $theme) { + if (access::can("view_full", $theme->item)) { + if (module::get_var("downloadfullsize", "fButton")) { + $downloadLink = url::site("downloadfullsize/send/{$theme->item->id}"); + $menu + ->append(Menu::factory("link") + ->id("downloadfullsize") + ->label(t("Download Video")) + ->url($downloadLink) + ->css_id("g-download-fullsize-link")); + } + } + } + + static function admin_menu($menu, $theme) { + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("downloadfullsize") + ->label(t("Download Photo Links")) + ->url(url::site("admin/downloadfullsize"))); + } + +} diff --git a/modules/downloadfullsize/helpers/downloadfullsize_theme.php b/modules/downloadfullsize/helpers/downloadfullsize_theme.php new file mode 100644 index 0000000..080ff91 --- /dev/null +++ b/modules/downloadfullsize/helpers/downloadfullsize_theme.php @@ -0,0 +1,26 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2012 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 downloadfullsize_theme { + static function head($theme) { + if ($theme->item && access::can("view_full", $theme->item)) { + return $theme->css("downloadfullsize_menu.css"); + } + } +} diff --git a/modules/downloadfullsize/images/ico-view-downloadfullsize.png b/modules/downloadfullsize/images/ico-view-downloadfullsize.png Binary files differnew file mode 100644 index 0000000..ce7804d --- /dev/null +++ b/modules/downloadfullsize/images/ico-view-downloadfullsize.png diff --git a/modules/downloadfullsize/module.info b/modules/downloadfullsize/module.info new file mode 100644 index 0000000..b2cc391 --- /dev/null +++ b/modules/downloadfullsize/module.info @@ -0,0 +1,7 @@ +name = "DownloadFullsize" +description = "Displays a link to download the fullsize version of the current photo." +version = 1 +author_name = "rWatcher" +author_url = "http://codex.gallery2.org/User:RWatcher" +info_url = "http://codex.gallery2.org/Gallery3:Modules:downloadfullsize" +discuss_url = "http://gallery.menalto.com/node/103278" diff --git a/modules/downloadfullsize/views/admin_downloadfullsize.html.php b/modules/downloadfullsize/views/admin_downloadfullsize.html.php new file mode 100644 index 0000000..5dc5cef --- /dev/null +++ b/modules/downloadfullsize/views/admin_downloadfullsize.html.php @@ -0,0 +1,5 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-download-fullsize-admin"> + <h2> <?= t("Download Fullsize Links") ?> </h2> + <?= $downloadlinks_form ?> +</div> diff --git a/modules/downloadfullsize/views/downloadfullsize_block.html.php b/modules/downloadfullsize/views/downloadfullsize_block.html.php new file mode 100644 index 0000000..a9ccc2a --- /dev/null +++ b/modules/downloadfullsize/views/downloadfullsize_block.html.php @@ -0,0 +1,18 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> + +<? if ($theme->item->is_photo()) { ?> +<div class="g-download-fullsize-block"> +<a href="<?= url::site("downloadfullsize/send/{$theme->item->id}") ?>" + title="<?= t("Download Photo") ?>" + class="g-button ui-icon-left ui-state-default ui-corner-all"><?= t("Download Fullsize Image") ?></a> +</div> +<? } ?> + +<? if ($theme->item->is_movie()) { ?> +<div class="g-download-fullsize-block"> +<a href="<?= url::site("downloadfullsize/send/{$theme->item->id}") ?>" + title="<?= t("Download Video") ?>" + class="g-button ui-icon-left ui-state-default ui-corner-all"><?= t("Download Movie") ?></a> +</div> +<? } ?> + diff --git a/modules/exif/controllers/exif.php b/modules/exif/controllers/exif.php new file mode 100644 index 0000000..aea8012 --- /dev/null +++ b/modules/exif/controllers/exif.php @@ -0,0 +1,33 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Exif_Controller extends Controller { + /** + * Display the EXIF data for an item. + */ + public function show($item_id) { + $item = ORM::factory("item", $item_id); + access::required("view", $item); + + $view = new View("exif_dialog.html"); + $view->details = exif::get($item); + + print $view; + } +} diff --git a/modules/exif/helpers/exif.php b/modules/exif/helpers/exif.php new file mode 100644 index 0000000..b17f460 --- /dev/null +++ b/modules/exif/helpers/exif.php @@ -0,0 +1,167 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling exif data. + */ +class exif_Core { + + protected static $exif_keys; + + static function extract($item) { + $keys = array(); + // Only try to extract EXIF from photos + if ($item->is_photo() && $item->mime_type == "image/jpeg") { + $data = array(); + require_once(MODPATH . "exif/lib/exif.php"); + $exif_raw = read_exif_data_raw($item->file_path(), false); + if (isset($exif_raw['ValidEXIFData'])) { + foreach(self::_keys() as $field => $exifvar) { + if (isset($exif_raw[$exifvar[0]][$exifvar[1]])) { + $value = $exif_raw[$exifvar[0]][$exifvar[1]]; + $value = encoding::convert_to_utf8($value); + $keys[$field] = Input::clean($value); + + if ($field == "DateTime") { + $time = strtotime($value); + if ($time > 0) { + $item->captured = $time; + } + } else if ($field == "Caption" && !$item->description) { + $item->description = $value; + } + } + } + } + + $size = getimagesize($item->file_path(), $info); + if (is_array($info) && !empty($info["APP13"])) { + $iptc = iptcparse($info["APP13"]); + foreach (array("Keywords" => "2#025", "Caption" => "2#120") as $keyword => $iptc_key) { + if (!empty($iptc[$iptc_key])) { + $value = implode(" ", $iptc[$iptc_key]); + $value = encoding::convert_to_utf8($value); + $keys[$keyword] = Input::clean($value); + + if ($keyword == "Caption" && !$item->description) { + $item->description = $value; + } + } + } + } + } + $item->save(); + + $record = ORM::factory("exif_record")->where("item_id", "=", $item->id)->find(); + if (!$record->loaded()) { + $record->item_id = $item->id; + } + $record->data = serialize($keys); + $record->key_count = count($keys); + $record->dirty = 0; + $record->save(); + } + + static function get($item) { + $exif = array(); + $record = ORM::factory("exif_record") + ->where("item_id", "=", $item->id) + ->find(); + if (!$record->loaded()) { + return array(); + } + + $definitions = self::_keys(); + $keys = unserialize($record->data); + foreach ($keys as $key => $value) { + $exif[] = array("caption" => $definitions[$key][2], "value" => $value); + } + + return $exif; + } + + private static function _keys() { + if (!isset(self::$exif_keys)) { + self::$exif_keys = array( + "Make" => array("IFD0", "Make", t("Camera Maker"), ), + "Model" => array("IFD0", "Model", t("Camera Model"), ), + "Aperture" => array("SubIFD", "FNumber", t("Aperture"), ), + "ColorSpace" => array("SubIFD", "ColorSpace", t("Color Space"), ), + "ExposureBias" => array("SubIFD", "ExposureBiasValue", t("Exposure Value"), ), + "ExposureProgram" => array("SubIFD", "ExposureProgram", t("Exposure Program"), ), + "ExposureTime" => array("SubIFD", "ExposureTime", t("Exposure Time"), ), + "Flash" => array("SubIFD", "Flash", t("Flash"), ), + "FocalLength" => array("SubIFD", "FocalLength", t("Focal Length"), ), + "ISO" => array("SubIFD", "ISOSpeedRatings", t("ISO"), ), + "MeteringMode" => array("SubIFD", "MeteringMode", t("Metering Mode"), ), + "DateTime" => array("SubIFD", "DateTimeOriginal", t("Date/Time"), ), + "Copyright" => array("IFD0", "Copyright", t("Copyright"), ), + "ImageType" => array("IFD0", "ImageType", t("Image Type"), ), + "Orientation" => array("IFD0", "Orientation", t("Orientation"), ), + "ResolutionUnit" => array("IFD0", "ResolutionUnit", t("Resolution Unit"), ), + "xResolution" => array("IFD0", "xResolution", t("X Resolution"), ), + "yResolution" => array("IFD0", "yResolution", t("Y Resolution"), ), + "Compression" => array("IFD1", "Compression", t("Compression"), ), + "BrightnessValue" => array("SubIFD", "BrightnessValue", t("Brightness Value"), ), + "Contrast" => array("SubIFD", "Contrast", t("Contrast"), ), + "ExposureMode" => array("SubIFD", "ExposureMode", t("Exposure Mode"), ), + "FlashEnergy" => array("SubIFD", "FlashEnergy", t("Flash Energy"), ), + "Saturation" => array("SubIFD", "Saturation", t("Saturation"), ), + "SceneType" => array("SubIFD", "SceneType", t("Scene Type"), ), + "Sharpness" => array("SubIFD", "Sharpness", t("Sharpness"), ), + "SubjectDistance" => array("SubIFD", "SubjectDistance", t("Subject Distance"), ), + "Caption" => array("IPTC", "Caption", t("Caption"), ), + "Keywords" => array("IPTC", "Keywords", t("Keywords"), ) + ); + } + return self::$exif_keys; + } + + static function stats() { + $missing_exif = db::build() + ->select("items.id") + ->from("items") + ->join("exif_records", "items.id", "exif_records.item_id", "left") + ->where("type", "=", "photo") + ->and_open() + ->where("exif_records.item_id", "IS", null) + ->or_where("exif_records.dirty", "=", 1) + ->close() + ->execute() + ->count(); + + $total_items = ORM::factory("item")->where("type", "=", "photo")->count_all(); + if (!$total_items) { + return array(0, 0, 0); + } + return array($missing_exif, $total_items, + round(100 * (($total_items - $missing_exif) / $total_items))); + } + + static function check_index() { + list ($remaining) = exif::stats(); + if ($remaining) { + site_status::warning( + t('Your Exif index needs to be updated. <a href="%url" class="g-dialog-link">Fix this now</a>', + array("url" => html::mark_clean(url::site("admin/maintenance/start/exif_task::update_index?csrf=__CSRF__")))), + "exif_index_out_of_date"); + } + } +} diff --git a/modules/exif/helpers/exif_event.php b/modules/exif/helpers/exif_event.php new file mode 100644 index 0000000..cd5068f --- /dev/null +++ b/modules/exif/helpers/exif_event.php @@ -0,0 +1,39 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class exif_event_Core { + static function item_created($item) { + if (!$item->is_album()) { + exif::extract($item); + } + } + + static function item_updated_data_file($item) { + if (!$item->is_album()) { + exif::extract($item); + } + } + + static function item_deleted($item) { + db::build() + ->delete("exif_records") + ->where("item_id", "=", $item->id) + ->execute(); + } +} diff --git a/modules/exif/helpers/exif_installer.php b/modules/exif/helpers/exif_installer.php new file mode 100644 index 0000000..75d0f83 --- /dev/null +++ b/modules/exif/helpers/exif_installer.php @@ -0,0 +1,45 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class exif_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {exif_records} ( + `id` int(9) NOT NULL auto_increment, + `item_id` INTEGER(9) NOT NULL, + `key_count` INTEGER(9) default 0, + `data` TEXT, + `dirty` BOOLEAN default 1, + PRIMARY KEY (`id`), + KEY(`item_id`)) + DEFAULT CHARSET=utf8;"); + } + + static function activate() { + exif::check_index(); + } + + static function deactivate() { + site_status::clear("exif_index_out_of_date"); + } + + static function uninstall() { + Database::instance()->query("DROP TABLE IF EXISTS {exif_records};"); + } +} diff --git a/modules/exif/helpers/exif_task.php b/modules/exif/helpers/exif_task.php new file mode 100644 index 0000000..f8a108a --- /dev/null +++ b/modules/exif/helpers/exif_task.php @@ -0,0 +1,88 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class exif_task_Core { + static function available_tasks() { + // Delete extra exif_records + db::build() + ->delete("exif_records") + ->where("item_id", "NOT IN", + db::build()->select("id")->from("items")->where("type", "=", "photo")) + ->execute(); + + list ($remaining, $total, $percent) = exif::stats(); + return array(Task_Definition::factory() + ->callback("exif_task::update_index") + ->name(t("Extract Exif data")) + ->description($remaining + ? t2("1 photo needs to be scanned", + "%count (%percent%) of your photos need to be scanned", + $remaining, array("percent" => (100 - $percent))) + : t("Exif data is up-to-date")) + ->severity($remaining ? log::WARNING : log::SUCCESS)); + } + + static function update_index($task) { + try { + $completed = $task->get("completed", 0); + + $start = microtime(true); + foreach (ORM::factory("item") + ->join("exif_records", "items.id", "exif_records.item_id", "left") + ->where("type", "=", "photo") + ->and_open() + ->where("exif_records.item_id", "IS", null) + ->or_where("exif_records.dirty", "=", 1) + ->close() + ->find_all(100) as $item) { + // The query above can take a long time, so start the timer after its done + // to give ourselves a little time to actually process rows. + if (!isset($start)) { + $start = microtime(true); + } + + exif::extract($item); + $completed++; + + if (microtime(true) - $start > .75) { + break; + } + } + + list ($remaining, $total, $percent) = exif::stats(); + $task->set("completed", $completed); + if ($remaining == 0 || !($remaining + $completed)) { + $task->done = true; + $task->state = "success"; + site_status::clear("exif_index_out_of_date"); + $task->percent_complete = 100; + } else { + $task->percent_complete = round(100 * $completed / ($remaining + $completed)); + } + $task->status = t2("one record updated, index is %percent% up-to-date", + "%count records updated, index is %percent% up-to-date", + $completed, array("percent" => $percent)); + } catch (Exception $e) { + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $task->log((string)$e); + } + } +} diff --git a/modules/exif/helpers/exif_theme.php b/modules/exif/helpers/exif_theme.php new file mode 100644 index 0000000..df7c6f4 --- /dev/null +++ b/modules/exif/helpers/exif_theme.php @@ -0,0 +1,38 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class exif_theme_Core { + static function sidebar_bottom($theme) { + $item = $theme->item(); + if ($item && $item->is_photo()) { + $record = db::build() + ->select("key_count") + ->from("exif_records") + ->where("item_id", "=", $item->id) + ->execute() + ->current(); + if ($record && $record->key_count) { + $view = new View("exif_sidebar.html"); + $view->item = $item; + return $view; + } + } + return null; + } +} diff --git a/modules/exif/lib/exif.php b/modules/exif/lib/exif.php new file mode 100644 index 0000000..8ba85c8 --- /dev/null +++ b/modules/exif/lib/exif.php @@ -0,0 +1,1135 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/* + Exifer 1.6 + Extracts EXIF information from digital photos. + + Originally created by: + Copyright © 2005 Jake Olefsky + http:// www.offsky.com/software/exif/index.php + jake@olefsky.com + + 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. http:// www.gnu.org/copyleft/gpl.html + + SUMMARY: + This script will correctly parse all of the EXIF data included in images taken + with digital cameras. It will read the IDF0, IDF1, SubIDF and InteroperabilityIFD + fields as well as parsing some of the MakerNote fields that vary depending on + camera make and model. This script parses more tags than the internal PHP exif + implementation and it will correctly identify and decode what all the values mean. + + This version will correctly parse the MakerNote field for Nikon, Olympus, and Canon + digital cameras. Others will follow. + + TESTED WITH: + Nikon CoolPix 700 + Nikon CoolPix E3200 + Nikon CoolPix 4500 + Nikon CoolPix 950 + Nikon Coolpix 5700 + Canon PowerShot S200 + Canon PowerShot S110 + Olympus C2040Z + Olympus C960 + Olumpus E-300 + Olympus E-410 + Olympus E-500 + Olympus E-510 + Olympus E-3 + Canon Ixus + Canon EOS 300D + Canon Digital Rebel + Canon EOS 10D + Canon PowerShot G2 + FujiFilm DX 10 + FujiFilm MX-1200 + FujiFilm FinePix2400 + FujiFilm FinePix2600 + FujiFilm FinePix S602 + FujiFilm FinePix40i + Sony D700 + Sony Cybershot + Kodak DC210 + Kodak DC240 + Kodak DC4800 + Kodak DX3215 + Ricoh RDC-5300 + Sanyo VPC-G250 + Sanyo VPC-SX550 + Epson 3100z + + + VERSION HISTORY: + + 1.0 September 23, 2002 + + + First Public Release + + 1.1 January 25, 2003 + + + Gracefully handled the error case where you pass an empty string to this library + + Fixed an inconsistency in the Olympus Camera parsing module + + Added support for parsing the MakerNote of Canon images. + + Modified how the imagefile is opened so it works for windows machines. + + Correctly parses the FocalPlaneResolutionUnit and PhotometricInterpretation fields + + Negative rational numbers are properly displayed + + Strange old cameras that use Motorola endineness are now properly supported + + Tested with several more cameras + + Potential Problem: Negative Shorts and Negative Longs may not be correctly displayed, but I + have not yet found an example of negative shorts or longs being used. + + 1.2 March 30, 2003 + + + Fixed an error that was displayed if you edited your image with WinXP's image viewer + + Fixed a bug that caused some images saved from 3rd party software to not parse correctly + + Changed the ExposureTime tag to display in fractional seconds rather than decimal + + Updated the ShutterSpeedValue tag to have the units of 'sec' + + Added support for parsing the MakeNote of FujiFilm images + + Added support for parsing the MakeNote of Sanyo images + + Fixed a bug with parsing some Olympus MakerNote tags + + Tested with several more cameras + + 1.3 June 15, 2003 + + + Fixed Canon MakerNote support for some models + (Canon has very difficult and inconsistent MakerNote syntax) + + Negative signed shorts and negative signed longs are properly displayed + + Several more tags are defined + + More information in my comments about what each tag is + + Parses and Displays GPS information if available + + Tested with several more cameras + + 1.4 September 14, 2003 + + + This software is now licensed under the GNU General Public License + + Exposure time is now correctly displayed when the numerator is 10 + + Fixed the calculation and display of ShutterSpeedValue, ApertureValue and MaxApertureValue + + Fixed a bug with the GPS code + + Tested with several more cameras + + 1.5 February 18, 2005 + + + It now gracefully deals with a passed in file that cannot be found. + + Fixed a GPS bug for the parsing of Altitude and other signed rational numbers + + Defined more values for Canon cameras. + + Added 'bulb' detection for ShutterSpeed + + Made script loading a little faster and less memory intensive. + + Bug fixes + + Better error reporting + + Graceful failure for files with corrupt exif info. + + QuickTime (including iPhoto) messes up the Makernote tag for certain photos (no workaround yet) + + Now reads exif information when the jpeg markers are out of order + + Gives raw data output for IPTC, COM and APP2 fields which are sometimes set by other applications + + Improvements to Nikon Makernote parsing + + 1.6 March 25th, 2007 [Zenphoto] + + + Adopted into the Zenphoto gallery project, at http://www.zenphoto.org + + Fixed a bug where strings had trailing null bytes. + + Formatted selected strings better. + + Added calculation of 35mm-equivalent focal length when possible. + + Cleaned up code for readability and efficiency. + + 1.7 April 11th, 2008 [Zenphoto] + + + Fixed bug with newer Olympus cameras where number of fields was miscalculated leading to bad performance. + + More logical fraction calculation for shutter speed. + +2009: For all further changes, see the Zenphoto change logs. + +*/ + + + + + + +//================================================================================================ +// Converts from Intel to Motorola endien. Just reverses the bytes (assumes hex is passed in) +//================================================================================================ + +function intel2Moto($intel) { + static $cache = array(); + if (isset($cache[$intel])) { + return $cache[$intel]; + } + + $cache[$intel] = ''; + $len = strlen($intel); + if ($len > 1000) { // an unreasonable length, override it. + $len = 1000; + } + for($i = 0; $i <= $len; $i += 2) { + $cache[$intel] .= substr($intel, $len-$i, 2); + } + return $cache[$intel]; +} + + +//================================================================================================ +// Looks up the name of the tag +//================================================================================================ +function lookup_tag($tag) { + switch($tag) { + // used by IFD0 'Camera Tags' + case '000b': $tag = 'ACDComment'; break; // text string up to 999 bytes long + case '00fe': $tag = 'ImageType'; break; // integer -2147483648 to 2147483647 + case '0106': $tag = 'PhotometricInterpret'; break; // ?? Please send sample image with this tag + case '010e': $tag = 'ImageDescription'; break; // text string up to 999 bytes long + case '010f': $tag = 'Make'; break; // text string up to 999 bytes long + case '0110': $tag = 'Model'; break; // text string up to 999 bytes long + case '0112': $tag = 'Orientation'; break; // integer values 1-9 + case '0115': $tag = 'SamplePerPixel'; break; // integer 0-65535 + case '011a': $tag = 'xResolution'; break; // positive rational number + case '011b': $tag = 'yResolution'; break; // positive rational number + case '011c': $tag = 'PlanarConfig'; break; // integer values 1-2 + case '0128': $tag = 'ResolutionUnit'; break; // integer values 1-3 + case '0131': $tag = 'Software'; break; // text string up to 999 bytes long + case '0132': $tag = 'DateTime'; break; // YYYY:MM:DD HH:MM:SS + case '013b': $tag = 'Artist'; break; // text string up to 999 bytes long + case '013c': $tag = 'HostComputer'; break; // text string + case '013e': $tag = 'WhitePoint'; break; // two positive rational numbers + case '013f': $tag = 'PrimaryChromaticities'; break; // six positive rational numbers + case '0211': $tag = 'YCbCrCoefficients'; break; // three positive rational numbers + case '0213': $tag = 'YCbCrPositioning'; break; // integer values 1-2 + case '0214': $tag = 'ReferenceBlackWhite'; break; // six positive rational numbers + case '8298': $tag = 'Copyright'; break; // text string up to 999 bytes long + case '8649': $tag = 'PhotoshopSettings'; break; // ?? + case '8769': $tag = 'ExifOffset'; break; // positive integer + case '8825': $tag = 'GPSInfoOffset'; break; + case '9286': $tag = 'UserCommentOld'; break; // ?? + // used by Exif SubIFD 'Image Tags' + case '829a': $tag = 'ExposureTime'; break; // seconds or fraction of seconds 1/x + case '829d': $tag = 'FNumber'; break; // positive rational number + case '8822': $tag = 'ExposureProgram'; break; // integer value 1-9 + case '8824': $tag = 'SpectralSensitivity'; break; // ?? + case '8827': $tag = 'ISOSpeedRatings'; break; // integer 0-65535 + case '9000': $tag = 'ExifVersion'; break; // ?? + case '9003': $tag = 'DateTimeOriginal'; break; // YYYY:MM:DD HH:MM:SS + case '9004': $tag = 'DateTimeDigitized'; break; // YYYY:MM:DD HH:MM:SS + case '9101': $tag = 'ComponentsConfiguration'; break; // ?? + case '9102': $tag = 'CompressedBitsPerPixel'; break; // positive rational number + case '9201': $tag = 'ShutterSpeedValue'; break; // seconds or fraction of seconds 1/x + case '9202': $tag = 'ApertureValue'; break; // positive rational number + case '9203': $tag = 'BrightnessValue'; break; // positive rational number + case '9204': $tag = 'ExposureBiasValue'; break; // positive rational number (EV) + case '9205': $tag = 'MaxApertureValue'; break; // positive rational number + case '9206': $tag = 'SubjectDistance'; break; // positive rational number (meters) + case '9207': $tag = 'MeteringMode'; break; // integer 1-6 and 255 + case '9208': $tag = 'LightSource'; break; // integer 1-255 + case '9209': $tag = 'Flash'; break; // integer 1-255 + case '920a': $tag = 'FocalLength'; break; // positive rational number (mm) + case '9213': $tag = 'ImageHistory'; break; // text string up to 999 bytes long + case '927c': $tag = 'MakerNote'; break; // a bunch of data + case '9286': $tag = 'UserComment'; break; // text string + case '9290': $tag = 'SubsecTime'; break; // text string up to 999 bytes long + case '9291': $tag = 'SubsecTimeOriginal'; break; // text string up to 999 bytes long + case '9292': $tag = 'SubsecTimeDigitized'; break; // text string up to 999 bytes long + case 'a000': $tag = 'FlashPixVersion'; break; // ?? + case 'a001': $tag = 'ColorSpace'; break; // values 1 or 65535 + case 'a002': $tag = 'ExifImageWidth'; break; // ingeter 1-65535 + case 'a003': $tag = 'ExifImageHeight'; break; // ingeter 1-65535 + case 'a004': $tag = 'RelatedSoundFile'; break; // text string 12 bytes long + case 'a005': $tag = 'ExifInteroperabilityOffset'; break; // positive integer + case 'a20c': $tag = 'SpacialFreqResponse'; break; // ?? + case 'a20b': $tag = 'FlashEnergy'; break; // positive rational number + case 'a20e': $tag = 'FocalPlaneXResolution'; break; // positive rational number + case 'a20f': $tag = 'FocalPlaneYResolution'; break; // positive rational number + case 'a210': $tag = 'FocalPlaneResolutionUnit'; break; // values 1-3 + case 'a214': $tag = 'SubjectLocation'; break; // two integers 0-65535 + case 'a215': $tag = 'ExposureIndex'; break; // positive rational number + case 'a217': $tag = 'SensingMethod'; break; // values 1-8 + case 'a300': $tag = 'FileSource'; break; // integer + case 'a301': $tag = 'SceneType'; break; // integer + case 'a302': $tag = 'CFAPattern'; break; // undefined data type + case 'a401': $tag = 'CustomerRender'; break; // values 0 or 1 + case 'a402': $tag = 'ExposureMode'; break; // values 0-2 + case 'a403': $tag = 'WhiteBalance'; break; // values 0 or 1 + case 'a404': $tag = 'DigitalZoomRatio'; break; // positive rational number + case 'a405': $tag = 'FocalLengthIn35mmFilm'; break; + case 'a406': $tag = 'SceneCaptureMode'; break; // values 0-3 + case 'a407': $tag = 'GainControl'; break; // values 0-4 + case 'a408': $tag = 'Contrast'; break; // values 0-2 + case 'a409': $tag = 'Saturation'; break; // values 0-2 + case 'a40a': $tag = 'Sharpness'; break; // values 0-2 + + // used by Interoperability IFD + case '0001': $tag = 'InteroperabilityIndex'; break; // text string 3 bytes long + case '0002': $tag = 'InteroperabilityVersion'; break; // datatype undefined + case '1000': $tag = 'RelatedImageFileFormat'; break; // text string up to 999 bytes long + case '1001': $tag = 'RelatedImageWidth'; break; // integer in range 0-65535 + case '1002': $tag = 'RelatedImageLength'; break; // integer in range 0-65535 + + // used by IFD1 'Thumbnail' + case '0100': $tag = 'ImageWidth'; break; // integer in range 0-65535 + case '0101': $tag = 'ImageLength'; break; // integer in range 0-65535 + case '0102': $tag = 'BitsPerSample'; break; // integers in range 0-65535 + case '0103': $tag = 'Compression'; break; // values 1 or 6 + case '0106': $tag = 'PhotometricInterpretation'; break;// values 0-4 + case '010e': $tag = 'ThumbnailDescription'; break; // text string up to 999 bytes long + case '010f': $tag = 'ThumbnailMake'; break; // text string up to 999 bytes long + case '0110': $tag = 'ThumbnailModel'; break; // text string up to 999 bytes long + case '0111': $tag = 'StripOffsets'; break; // ?? + case '0112': $tag = 'ThumbnailOrientation'; break; // integer 1-9 + case '0115': $tag = 'SamplesPerPixel'; break; // ?? + case '0116': $tag = 'RowsPerStrip'; break; // ?? + case '0117': $tag = 'StripByteCounts'; break; // ?? + case '011a': $tag = 'ThumbnailXResolution'; break; // positive rational number + case '011b': $tag = 'ThumbnailYResolution'; break; // positive rational number + case '011c': $tag = 'PlanarConfiguration'; break; // values 1 or 2 + case '0128': $tag = 'ThumbnailResolutionUnit'; break; // values 1-3 + case '0201': $tag = 'JpegIFOffset'; break; + case '0202': $tag = 'JpegIFByteCount'; break; + case '0212': $tag = 'YCbCrSubSampling'; break; + + // misc + case '00ff': $tag = 'SubfileType'; break; + case '012d': $tag = 'TransferFunction'; break; + case '013d': $tag = 'Predictor'; break; + case '0142': $tag = 'TileWidth'; break; + case '0143': $tag = 'TileLength'; break; + case '0144': $tag = 'TileOffsets'; break; + case '0145': $tag = 'TileByteCounts'; break; + case '014a': $tag = 'SubIFDs'; break; + case '015b': $tag = 'JPEGTables'; break; + case '828d': $tag = 'CFARepeatPatternDim'; break; + case '828e': $tag = 'CFAPattern'; break; + case '828f': $tag = 'BatteryLevel'; break; + case '83bb': $tag = 'IPTC/NAA'; break; + case '8773': $tag = 'InterColorProfile'; break; + + case '8828': $tag = 'OECF'; break; + case '8829': $tag = 'Interlace'; break; + case '882a': $tag = 'TimeZoneOffset'; break; + case '882b': $tag = 'SelfTimerMode'; break; + case '920b': $tag = 'FlashEnergy'; break; + case '920c': $tag = 'SpatialFrequencyResponse'; break; + case '920d': $tag = 'Noise'; break; + case '9211': $tag = 'ImageNumber'; break; + case '9212': $tag = 'SecurityClassification'; break; + case '9214': $tag = 'SubjectLocation'; break; + case '9215': $tag = 'ExposureIndex'; break; + case '9216': $tag = 'TIFF/EPStandardID'; break; + case 'a20b': $tag = 'FlashEnergy'; break; + + default: $tag = 'unknown:'.$tag; break; + } + return $tag; + +} + + +//================================================================================================ +// Looks up the datatype +//================================================================================================ +function lookup_type(&$type,&$size) { + switch($type) { + case '0001': $type = 'UBYTE'; $size=1; break; + case '0002': $type = 'ASCII'; $size=1; break; + case '0003': $type = 'USHORT'; $size=2; break; + case '0004': $type = 'ULONG'; $size=4; break; + case '0005': $type = 'URATIONAL'; $size=8; break; + case '0006': $type = 'SBYTE'; $size=1; break; + case '0007': $type = 'UNDEFINED'; $size=1; break; + case '0008': $type = 'SSHORT'; $size=2; break; + case '0009': $type = 'SLONG'; $size=4; break; + case '000a': $type = 'SRATIONAL'; $size=8; break; + case '000b': $type = 'FLOAT'; $size=4; break; + case '000c': $type = 'DOUBLE'; $size=8; break; + default: $type = 'error:'.$type; $size=0; break; + } + return $type; +} + +//================================================================================================ +// processes a irrational number +//================================================================================================ +function unRational($data, $type, $intel) { + $data = bin2hex($data); + if ($intel == 1) { + $data = intel2Moto($data); + $top = hexdec(substr($data,8,8)); // intel stores them bottom-top + $bottom = hexdec(substr($data,0,8)); // intel stores them bottom-top + } else { + $top = hexdec(substr($data,0,8)); // motorola stores them top-bottom + $bottom = hexdec(substr($data,8,8)); // motorola stores them top-bottom + } + + if ($type == 'SRATIONAL' && $top > 2147483647) $top = $top - 4294967296; // this makes the number signed instead of unsigned + if ($bottom != 0) + $data=$top/$bottom; + else + if ($top == 0) + $data = 0; + else + $data = $top.'/'.$bottom; + return $data; +} + +//================================================================================================ +// processes a rational number +//================================================================================================ +function rational($data,$type,$intel) { + if (($type == 'USHORT' || $type == 'SSHORT')) { + $data = substr($data,0,2); + } + $data = bin2hex($data); + if ($intel == 1) { + $data = intel2Moto($data); + } + $data = hexdec($data); + if ($type == 'SSHORT' && $data > 32767) $data = $data - 65536; // this makes the number signed instead of unsigned + if ($type == 'SLONG' && $data > 2147483647) $data = $data - 4294967296; // this makes the number signed instead of unsigned + return $data; +} + +//================================================================================================ +// Formats Data for the data type +//================================================================================================ +function formatData($type,$tag,$intel,$data) { + switch ($type) { + case 'ASCII': + if (($pos = strpos($data, chr(0))) !== false) { // Search for a null byte and stop there. + $data = substr($data, 0, $pos); + } + if ($tag == '010f') $data = ucwords(strtolower(trim($data))); // Format certain kinds of strings nicely (Camera make etc.) + break; + case 'URATIONAL': + case 'SRATIONAL': + switch ($tag) { + case '011a': // XResolution + case '011b': // YResolution + $data = round(unRational($data,$type,$intel)).' dots per ResolutionUnit'; + break; + case '829a': // Exposure Time + $data = formatExposure(unRational($data,$type,$intel)); + break; + case '829d': // FNumber + $data = 'f/'.unRational($data,$type,$intel); + break; + case '9204': // ExposureBiasValue + $data = round(unRational($data,$type,$intel), 2) . ' EV'; + break; + case '9205': // ApertureValue + case '9202': // MaxApertureValue + // ApertureValue is given in the APEX Mode. Many thanks to Matthieu Froment for this code + // The formula is : Aperture = 2*log2(FNumber) <=> FNumber = e((Aperture.ln(2))/2) + $datum = exp((unRational($data,$type,$intel)*log(2))/2); + $data = round($datum, 1);// Focal is given with a precision of 1 digit. + $data='f/'.$datum; + break; + case '920a': // FocalLength + $data = unRational($data,$type,$intel).' mm'; + break; + case '9201': // ShutterSpeedValue + // The ShutterSpeedValue is given in the APEX mode. Many thanks to Matthieu Froment for this code + // The formula is : Shutter = - log2(exposureTime) (Appendix C of EXIF spec.) + // Where shutter is in APEX, log2(exposure) = ln(exposure)/ln(2) + // So final formula is : exposure = exp(-ln(2).shutter) + // The formula can be developed : exposure = 1/(exp(ln(2).shutter)) + $datum = exp(unRational($data,$type,$intel) * log(2)); + if ($datum != 0) $datum = 1/$datum; + $data = formatExposure($datum); + break; + default: + $data = unRational($data,$type,$intel); + break; + } + break; + case 'USHORT': + case 'SSHORT': + case 'ULONG': + case 'SLONG': + case 'FLOAT': + case 'DOUBLE': + $data = rational($data,$type,$intel); + switch ($tag) { + case '0112': // Orientation + // Example of how all of these tag formatters should be... + switch ($data) { + case 0 : // not set, presume normal + case 1 : $data = (string) t('1: Normal (0 deg)'); break; + case 2 : $data = (string) t('2: Mirrored'); break; + case 3 : $data = (string) t('3: Upside-down'); break; + case 4 : $data = (string) t('4: Upside-down Mirrored'); break; + case 5 : $data = (string) t('5: 90 deg CW Mirrored'); break; + case 6 : $data = (string) t('6: 90 deg CCW'); break; + case 7 : $data = (string) t('7: 90 deg CCW Mirrored'); break; + case 8 : $data = (string) t('8: 90 deg CW'); break; + default : $data = sprintf((string) t('%d: Unknown'),$data); break; + } + break; + case '0128': // ResolutionUnit + case 'a210': // FocalPlaneResolutionUnit + case '0128': // ThumbnailResolutionUnit + switch ($data) { + case 1: $data = (string) t('No Unit'); break; + case 2: $data = (string) t('Inch'); break; + case 3: $data = (string) t('Centimeter'); break; + } + break; + case '0213': // YCbCrPositioning + switch ($data) { + case 1: $data = (string) t('Center of Pixel Array'); break; + case 2: $data = (string) t('Datum Point'); break; + } + break; + case '8822': // ExposureProgram + switch ($data) { + case 1: $data = (string) t('Manual'); break; + case 2: $data = (string) t('Program'); break; + case 3: $data = (string) t('Aperture Priority'); break; + case 4: $data = (string) t('Shutter Priority'); break; + case 5: $data = (string) t('Program Creative'); break; + case 6: $data = (string) t('Program Action'); break; + case 7: $data = (string) t('Portrait'); break; + case 8: $data = (string) t('Landscape'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case '9207': // MeteringMode + switch ($data) { + case 1: $data = (string) t('Average'); break; + case 2: $data = (string) t('Center Weighted Average'); break; + case 3: $data = (string) t('Spot'); break; + case 4: $data = (string) t('Multi-Spot'); break; + case 5: $data = (string) t('Pattern'); break; + case 6: $data = (string) t('Partial'); break; + case 255: $data = (string) t('Other'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case '9208': // LightSource + switch ($data) { + case 1: $data = (string) t('Daylight'); break; + case 2: $data = (string) t('Fluorescent'); break; + case 3: $data = (string) t('Tungsten'); break; // 3 Tungsten (Incandescent light) + // 4 Flash + // 9 Fine Weather + case 10: $data = (string) t('Flash'); break; // 10 Cloudy Weather + // 11 Shade + // 12 Daylight Fluorescent (D 5700 - 7100K) + // 13 Day White Fluorescent (N 4600 - 5400K) + // 14 Cool White Fluorescent (W 3900 -4500K) + // 15 White Fluorescent (WW 3200 - 3700K) + // 10 Flash + case 17: $data = (string) t('Standard Light A'); break; + case 18: $data = (string) t('Standard Light B'); break; + case 19: $data = (string) t('Standard Light C'); break; + case 20: $data = (string) t('D55'); break; + case 21: $data = (string) t('D65'); break; + case 22: $data = (string) t('D75'); break; + case 23: $data = (string) t('D50'); break; + case 24: $data = (string) t('ISO Studio Tungsten'); break; + case 255: $data = (string) t('Other'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case '9209': // Flash + switch ($data) { + case 0: $data = (string) t('No Flash'); break; + case 1: $data = (string) t('Flash'); break; + case 5: $data = (string) t('Flash, strobe return light not detected'); break; + case 7: $data = (string) t('Flash, strobe return light detected'); break; + case 9: $data = (string) t('Compulsory Flash'); break; + case 13: $data = (string) t('Compulsory Flash, Return light not detected'); break; + case 15: $data = (string) t('Compulsory Flash, Return light detected'); break; + case 16: $data = (string) t('No Flash'); break; + case 24: $data = (string) t('No Flash'); break; + case 25: $data = (string) t('Flash, Auto-Mode'); break; + case 29: $data = (string) t('Flash, Auto-Mode, Return light not detected'); break; + case 31: $data = (string) t('Flash, Auto-Mode, Return light detected'); break; + case 32: $data = (string) t('No Flash'); break; + case 65: $data = (string) t('Red Eye'); break; + case 69: $data = (string) t('Red Eye, Return light not detected'); break; + case 71: $data = (string) t('Red Eye, Return light detected'); break; + case 73: $data = (string) t('Red Eye, Compulsory Flash'); break; + case 77: $data = (string) t('Red Eye, Compulsory Flash, Return light not detected'); break; + case 79: $data = (string) t('Red Eye, Compulsory Flash, Return light detected'); break; + case 89: $data = (string) t('Red Eye, Auto-Mode'); break; + case 93: $data = (string) t('Red Eye, Auto-Mode, Return light not detected'); break; + case 95: $data = (string) t('Red Eye, Auto-Mode, Return light detected'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case 'a001': // ColorSpace + if ($data == 1) $data = (string) t('sRGB'); + else $data = (string) t('Uncalibrated'); + break; + case 'a002': // ExifImageWidth + case 'a003': // ExifImageHeight + $data = $data. ' '.(string) t('pixels'); + break; + case '0103': // Compression + switch ($data) { + case 1: $data = (string) t('No Compression'); break; + case 6: $data = (string) t('Jpeg Compression'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case 'a217': // SensingMethod + switch ($data) { + case 1: $data = (string) t('Not defined'); break; + case 2: $data = (string) t('One Chip Color Area Sensor'); break; + case 3: $data = (string) t('Two Chip Color Area Sensor'); break; + case 4: $data = (string) t('Three Chip Color Area Sensor'); break; + case 5: $data = (string) t('Color Sequential Area Sensor'); break; + case 7: $data = (string) t('Trilinear Sensor'); break; + case 8: $data = (string) t('Color Sequential Linear Sensor'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + case '0106': // PhotometricInterpretation + switch ($data) { + case 1: $data = (string) t('Monochrome'); break; + case 2: $data = (string) t('RGB'); break; + case 6: $data = (string) t('YCbCr'); break; + default: $data = (string) t('Unknown').': '.$data; break; + } + break; + //case "a408": // Contrast + //case "a40a": //Sharpness + // switch($data) { + // case 0: $data="Normal"; break; + // case 1: $data="Soft"; break; + // case 2: $data="Hard"; break; + // default: $data="Unknown"; break; + // } + // break; + //case "a409": // Saturation + // switch($data) { + // case 0: $data="Normal"; break; + // case 1: $data="Low saturation"; break; + // case 2: $data="High saturation"; break; + // default: $data="Unknown"; break; + // } + // break; + //case "a402": // Exposure Mode + // switch($data) { + // case 0: $data="Auto exposure"; break; + // case 1: $data="Manual exposure"; break; + // case 2: $data="Auto bracket"; break; + // default: $data="Unknown"; break; + // } + // break; + } + break; + case 'UNDEFINED': + switch ($tag) { + case '9000': // ExifVersion + case 'a000': // FlashPixVersion + case '0002': // InteroperabilityVersion + $data=(string) t('version').' '.$data/100; + break; + case 'a300': // FileSource + $data = bin2hex($data); + $data = str_replace('00','',$data); + $data = str_replace('03',(string) t('Digital Still Camera'),$data); + break; + case 'a301': // SceneType + $data = bin2hex($data); + $data = str_replace('00','',$data); + $data = str_replace('01',(string) t('Directly Photographed'),$data); + break; + case '9101': // ComponentsConfiguration + $data = bin2hex($data); + $data = str_replace('01','Y',$data); + $data = str_replace('02','Cb',$data); + $data = str_replace('03','Cr',$data); + $data = str_replace('04','R',$data); + $data = str_replace('05','G',$data); + $data = str_replace('06','B',$data); + $data = str_replace('00','',$data); + break; + //case "9286": //UserComment + // $encoding = rtrim(substr($data, 0, 8)); + // $data = rtrim(substr($data, 8)); + // break; + } + break; + default: + $data = bin2hex($data); + if ($intel == 1) $data = intel2Moto($data); + break; + } + return $data; +} + +function formatExposure($data) { + if (strpos($data,'/')===false) { + if ($data > 1) { + return round($data, 2).' '.(string) t('sec'); + } else { + $n=0; $d=0; + ConvertToFraction($data, $n, $d); + return $n.'/'.$d.' '.(string) t('sec'); + } + } else { + return (string) t('Bulb'); + } +} + +//================================================================================================ +// Reads one standard IFD entry +//================================================================================================ +function read_entry(&$result,$in,$seek,$intel,$ifd_name,$globalOffset) { + + if (feof($in)) { // test to make sure we can still read. + $result['Errors'] = $result['Errors']+1; + return; + } + + // 2 byte tag + $tag = bin2hex(fread($in, 2)); + if ($intel == 1) $tag = intel2Moto($tag); + $tag_name = lookup_tag($tag); + + // 2 byte datatype + $type = bin2hex(fread($in, 2)); + if ($intel == 1) $type = intel2Moto($type); + lookup_type($type, $size); + + if (strpos($tag_name, 'unknown:') !== false && strpos($type, 'error:') !== false) { // we have an error + $result['Errors'] = $result['Errors']+1; + return; + } + + // 4 byte number of elements + $count = bin2hex(fread($in, 4)); + if ($intel == 1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + // 4 byte value or pointer to value if larger than 4 bytes + $value = fread( $in, 4 ); + + if ($bytesofdata <= 4) { // if datatype is 4 bytes or less, its the value + $data = $value; + } else if ($bytesofdata < 100000) { // otherwise its a pointer to the value, so lets go get it + $value = bin2hex($value); + if ($intel == 1) $value = intel2Moto($value); + $v = fseek($seek, $globalOffset+hexdec($value)); // offsets are from TIFF header which is 12 bytes from the start of the file + if ($v == 0) { + $data = fread($seek, $bytesofdata); + } else if ($v == -1) { + $result['Errors'] = $result['Errors']+1; + } + } else { // bytesofdata was too big, so the exif had an error + $result['Errors'] = $result['Errors']+1; + return; + } + if ($tag_name == 'MakerNote') { // if its a maker tag, we need to parse this specially + $make = $result['IFD0']['Make']; + + if ($result['VerboseOutput'] == 1) { + $result[$ifd_name]['MakerNote']['RawData'] = $data; + } + if (preg_match('/NIKON/i',$make)) { + require_once(dirname(__FILE__).'/makers/nikon.php'); + parseNikon($data,$result); + $result[$ifd_name]['KnownMaker'] = 1; + } else if (preg_match('/OLYMPUS/i',$make)) { + require_once(dirname(__FILE__).'/makers/olympus.php'); + parseOlympus($data,$result,$seek,$globalOffset); + $result[$ifd_name]['KnownMaker'] = 1; + } else if (preg_match('/Canon/i',$make)) { + require_once(dirname(__FILE__).'/makers/canon.php'); + parseCanon($data,$result,$seek,$globalOffset); + $result[$ifd_name]['KnownMaker'] = 1; + } else if (preg_match('/FUJIFILM/i',$make)) { + require_once(dirname(__FILE__).'/makers/fujifilm.php'); + parseFujifilm($data,$result); + $result[$ifd_name]['KnownMaker'] = 1; + } else if (preg_match('/SANYO/i',$make)) { + require_once(dirname(__FILE__).'/makers/sanyo.php'); + parseSanyo($data,$result,$seek,$globalOffset); + $result[$ifd_name]['KnownMaker'] = 1; + } else if (preg_match('/Panasonic/i',$make)) { + require_once(dirname(__FILE__).'/makers/panasonic.php'); + parsePanasonic($data,$result,$seek,$globalOffset); + $result[$ifd_name]['KnownMaker'] = 1; + } else { + $result[$ifd_name]['KnownMaker'] = 0; + } + } else if ($tag_name == 'GPSInfoOffset') { + require_once(dirname(__FILE__).'/makers/gps.php'); + $formated_data = formatData($type,$tag,$intel,$data); + $result[$ifd_name]['GPSInfo'] = $formated_data; + parseGPS($data,$result,$formated_data,$seek,$globalOffset); + } else { + // Format the data depending on the type and tag + $formated_data = formatData($type,$tag,$intel,$data); + + $result[$ifd_name][$tag_name] = $formated_data; + + if ($result['VerboseOutput'] == 1) { + if ($type == 'URATIONAL' || $type == 'SRATIONAL' || $type == 'USHORT' || $type == 'SSHORT' || $type == 'ULONG' || $type == 'SLONG' || $type == 'FLOAT' || $type == 'DOUBLE') { + $data = bin2hex($data); + if ($intel == 1) $data = intel2Moto($data); + } + $result[$ifd_name][$tag_name.'_Verbose']['RawData'] = $data; + $result[$ifd_name][$tag_name.'_Verbose']['Type'] = $type; + $result[$ifd_name][$tag_name.'_Verbose']['Bytes'] = $bytesofdata; + } + } +} + + +//================================================================================================ +// Pass in a file and this reads the EXIF data +// +// Usefull resources +// http:// www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html +// http:// www.w3.org/Graphics/JPEG/jfif.txt +// http:// exif.org/ +// http:// www.ozhiker.com/electronics/pjmt/library/list_contents.php4 +// http:// www.ozhiker.com/electronics/pjmt/jpeg_info/makernotes.html +// http:// pel.sourceforge.net/ +// http:// us2.php.net/manual/en/function.exif-read-data.php +//================================================================================================ +function read_exif_data_raw($path,$verbose) { + + if ($path == '' || $path == 'none') return; + + $in = @fopen($path, 'rb'); // the b is for windows machines to open in binary mode + $seek = @fopen($path, 'rb'); // There may be an elegant way to do this with one file handle. + + $globalOffset = 0; + + if (!isset($verbose)) $verbose=0; + + $result['VerboseOutput'] = $verbose; + $result['Errors'] = 0; + + if (!$in || !$seek) { // if the path was invalid, this error will catch it + $result['Errors'] = 1; + $result['Error'][$result['Errors']] = (string) t('The file could not be found.'); + return $result; + } + + $GLOBALS['exiferFileSize'] = filesize($path); + + // First 2 bytes of JPEG are 0xFFD8 + $data = bin2hex(fread( $in, 2 )); + if ($data == 'ffd8') { + $result['ValidJpeg'] = 1; + } else { + $result['ValidJpeg'] = 0; + fseek($in, 0); + } + + $result['ValidIPTCData'] = 0; + $result['ValidJFIFData'] = 0; + $result['ValidEXIFData'] = 0; + $result['ValidAPP2Data'] = 0; + $result['ValidCOMData'] = 0; + +if ($result['ValidJpeg'] == 1) { + // Next 2 bytes are MARKER tag (0xFFE#) + $data = bin2hex(fread( $in, 2 )); + $size = bin2hex(fread( $in, 2 )); + + // LOOP THROUGH MARKERS TILL YOU GET TO FFE1 (exif marker) + $abortCount = 0; + while(!feof($in) && $data!='ffe1' && $data!='ffc0' && $data!='ffd9' && ++$abortCount < 200) { + if ($data == 'ffe0') { // JFIF Marker + $result['ValidJFIFData'] = 1; + $result['JFIF']['Size'] = hexdec($size); + + if (hexdec($size)-2 > 0) { + $data = fread( $in, hexdec($size)-2); + $result['JFIF']['Data'] = $data; + } + + $result['JFIF']['Identifier'] = substr($data,0,5);; + $result['JFIF']['ExtensionCode'] = bin2hex(substr($data,6,1)); + + $globalOffset+=hexdec($size)+2; + + } else if ($data == 'ffed') { // IPTC Marker + $result['ValidIPTCData'] = 1; + $result['IPTC']['Size'] = hexdec($size); + + if (hexdec($size)-2 > 0) { + $data = fread( $in, hexdec($size)-2); + $result['IPTC']['Data'] = $data ; + } + $globalOffset+=hexdec($size)+2; + + } else if ($data == 'ffe2') { // EXIF extension Marker + $result['ValidAPP2Data'] = 1; + $result['APP2']['Size'] = hexdec($size); + + if (hexdec($size)-2 > 0) { + $data = fread( $in, hexdec($size)-2); + $result['APP2']['Data'] = $data ; + } + $globalOffset+=hexdec($size)+2; + + } else if ($data == 'fffe') { // COM extension Marker + $result['ValidCOMData'] = 1; + $result['COM']['Size'] = hexdec($size); + + if (hexdec($size)-2 > 0) { + $data = fread( $in, hexdec($size)-2); + $result['COM']['Data'] = $data ; + } + $globalOffset+=hexdec($size)+2; + + } else if ($data == 'ffe1') { + $result['ValidEXIFData'] = 1; + } + + $data = bin2hex(fread( $in, 2 )); + $size = bin2hex(fread( $in, 2 )); + } + // END MARKER LOOP + + if ($data == 'ffe1') { + $result['ValidEXIFData'] = 1; + } else { + fclose($in); + fclose($seek); + return $result; + } + + // Size of APP1 + $result['APP1Size'] = hexdec($size); + + // Start of APP1 block starts with 'Exif' header (6 bytes) + $header = fread( $in, 6 ); + +} // END IF ValidJpeg + + // Then theres a TIFF header with 2 bytes of endieness (II or MM) + $header = fread( $in, 2 ); + if ($header==='II') { + $intel=1; + $result['Endien'] = 'Intel'; + } else if ($header==='MM') { + $intel=0; + $result['Endien'] = 'Motorola'; + } else { + $intel=1; // not sure what the default should be, but this seems reasonable + $result['Endien'] = 'Unknown'; + } + + // 2 bytes of 0x002a + $tag = bin2hex(fread( $in, 2 )); + + // Then 4 bytes of offset to IFD0 (usually 8 which includes all 8 bytes of TIFF header) + $offset = bin2hex(fread( $in, 4 )); + if ($intel == 1) $offset = intel2Moto($offset); + + // Check for extremely large values here + if (hexdec($offset) > 100000) { + $result['ValidEXIFData'] = 0; + fclose($in); + fclose($seek); + return $result; + } + + if (hexdec($offset)>8) $unknown = fread( $in, hexdec($offset)-8); // fixed this bug in 1.3 + + // add 12 to the offset to account for TIFF header + if ($result['ValidJpeg'] == 1) { + $globalOffset+=12; + } + + + //=========================================================== + // Start of IFD0 + $num = bin2hex(fread( $in, 2 )); + if ($intel == 1) $num = intel2Moto($num); + $num = hexdec($num); + $result['IFD0NumTags'] = $num; + + if ($num<1000) { // 1000 entries is too much and is probably an error. + for($i=0; $i<$num; $i++) { + read_entry($result,$in,$seek,$intel,'IFD0',$globalOffset); + } + } else { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = 'Illegal size for IFD0'; + } + + // store offset to IFD1 + $offset = bin2hex(fread( $in, 4 )); + if ($intel == 1) $offset = intel2Moto($offset); + $result['IFD1Offset'] = hexdec($offset); + + // Check for SubIFD + if (!isset($result['IFD0']['ExifOffset']) || $result['IFD0']['ExifOffset'] == 0) { + fclose($in); + fclose($seek); + return $result; + } + + // seek to SubIFD (Value of ExifOffset tag) above. + $ExitOffset = $result['IFD0']['ExifOffset']; + $v = fseek($in,$globalOffset+$ExitOffset); + if ($v == -1) { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Could not Find SubIFD'); + } + + //=========================================================== + // Start of SubIFD + $num = bin2hex(fread( $in, 2 )); + if ($intel == 1) $num = intel2Moto($num); + $num = hexdec($num); + $result['SubIFDNumTags'] = $num; + + if ($num<1000) { // 1000 entries is too much and is probably an error. + for($i=0; $i<$num; $i++) { + read_entry($result,$in,$seek,$intel,'SubIFD',$globalOffset); + } + } else { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Illegal size for SubIFD'); + } + + // Add the 35mm equivalent focal length: + if (isset($result['IFD0']['FocalLengthIn35mmFilm']) && !isset($result['SubIFD']['FocalLengthIn35mmFilm'])) { // found in the wrong place + $result['SubIFD']['FocalLengthIn35mmFilm'] = $result['IFD0']['FocalLengthIn35mmFilm']; + } + if (!isset($result['SubIFD']['FocalLengthIn35mmFilm'])) { + $result['SubIFD']['FocalLengthIn35mmFilm'] = get35mmEquivFocalLength($result); + } + + // Check for IFD1 + if (!isset($result['IFD1Offset']) || $result['IFD1Offset'] == 0) { + fclose($in); + fclose($seek); + return $result; + } + // seek to IFD1 + $v = fseek($in,$globalOffset+$result['IFD1Offset']); + if ($v == -1) { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Could not Find IFD1'); + } + + //=========================================================== + // Start of IFD1 + $num = bin2hex(fread( $in, 2 )); + if ($intel == 1) $num = intel2Moto($num); + $num = hexdec($num); + $result['IFD1NumTags'] = $num; + + if ($num<1000) { // 1000 entries is too much and is probably an error. + for($i=0; $i<$num; $i++) { + read_entry($result,$in,$seek,$intel,'IFD1',$globalOffset); + } + } else { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Illegal size for IFD1'); + } + // If verbose output is on, include the thumbnail raw data... + if ($result['VerboseOutput'] == 1 && $result['IFD1']['JpegIFOffset']>0 && $result['IFD1']['JpegIFByteCount']>0) { + $v = fseek($seek,$globalOffset+$result['IFD1']['JpegIFOffset']); + if ($v == 0) { + $data = fread($seek, $result['IFD1']['JpegIFByteCount']); + } else if ($v == -1) { + $result['Errors'] = $result['Errors']+1; + } + $result['IFD1']['ThumbnailData'] = $data; + } + + + // Check for Interoperability IFD + if (!isset($result['SubIFD']['ExifInteroperabilityOffset']) || $result['SubIFD']['ExifInteroperabilityOffset'] == 0) { + fclose($in); + fclose($seek); + return $result; + } + // Seek to InteroperabilityIFD + $v = fseek($in,$globalOffset+$result['SubIFD']['ExifInteroperabilityOffset']); + if ($v == -1) { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Could not Find InteroperabilityIFD'); + } + + //=========================================================== + // Start of InteroperabilityIFD + $num = bin2hex(fread( $in, 2 )); + if ($intel == 1) $num = intel2Moto($num); + $num = hexdec($num); + $result['InteroperabilityIFDNumTags'] = $num; + + if ($num<1000) { // 1000 entries is too much and is probably an error. + for($i=0; $i<$num; $i++) { + read_entry($result,$in,$seek,$intel,'InteroperabilityIFD',$globalOffset); + } + } else { + $result['Errors'] = $result['Errors']+1; + $result['Error'][$result['Errors']] = (string) t('Illegal size for InteroperabilityIFD'); + } + fclose($in); + fclose($seek); + return $result; +} + +//================================================================================================ +// Converts a floating point number into a fraction. Many thanks to Matthieu Froment for this code +//================================================================================================ +function ConvertToFraction($v, &$n, &$d) +{ + if ($v == 0) { + $n = 0; + $d = 1; + return; + } + for ($n=1; $n<100; $n++) { + $v1 = 1/$v*$n; + $d = round($v1, 0); + if (abs($d - $v1) < 0.02) return;// within tolarance + } +} + +//================================================================================================ +// Calculates the 35mm-equivalent focal length from the reported sensor resolution, by Tristan Harward. +//================================================================================================ +function get35mmEquivFocalLength(&$result) { + if (isset($result['SubIFD']['ExifImageWidth'])) { + $width = $result['SubIFD']['ExifImageWidth']; + } else { + $width = 0; + } + if (isset($result['SubIFD']['FocalPlaneResolutionUnit'])) { + $units = $result['SubIFD']['FocalPlaneResolutionUnit']; + } else { + $units = ''; + } + $unitfactor = 1; + switch ($units) { + case 'Inch' : $unitfactor = 25.4; break; + case 'Centimeter' : $unitfactor = 10; break; + case 'No Unit' : $unitfactor = 25.4; break; + default : $unitfactor = 25.4; + } + if (isset($result['SubIFD']['FocalPlaneXResolution'])) { + $xres = $result['SubIFD']['FocalPlaneXResolution']; + } else { + $xres = ''; + } + if (isset($result['SubIFD']['FocalLength'])) { + $fl = $result['SubIFD']['FocalLength']; + } else { + $fl = 0; + } + + if (($width != 0) && !empty($units) && !empty($xres) && !empty($fl) && !empty($width)) { + $ccdwidth = ($width * $unitfactor) / $xres; + $equivfl = $fl / $ccdwidth*36+0.5; + return $equivfl; + } + return null; +} + +?> diff --git a/modules/exif/lib/makers/canon.php b/modules/exif/lib/makers/canon.php new file mode 100644 index 0000000..aecd266 --- /dev/null +++ b/modules/exif/lib/makers/canon.php @@ -0,0 +1,426 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright © 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Canon_tag($tag) { + + switch($tag) { + case "0001": $tag = "Settings 1";break; + case "0004": $tag = "Settings 4";break; + case "0006": $tag = "ImageType";break; + case "0007": $tag = "FirmwareVersion";break; + case "0008": $tag = "ImageNumber";break; + case "0009": $tag = "OwnerName";break; + case "000c": $tag = "CameraSerialNumber";break; + case "000f": $tag = "CustomFunctions";break; + case "0095": $tag = "LensInfo";break; + + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatCanonData($type,$tag,$intel,$data,$exif,&$result) { + $place = 0; + + + if($type=="ASCII") { + $result = $data = str_replace("\0", "", $data); + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + $data = unRational($data,$type,$intel); + + if($tag=="0204") { //DigitalZoom + $data=$data."x"; + } + + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + + $data = rational($data,$type,$intel); + $result['RAWDATA'] = $data; + + if($tag=="0001") { //first chunk + $result['Bytes']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//0 + if ($result['Bytes'] != strlen($data) / 2) return $result; //Bad chunk + $result['Macro']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//1 + switch($result['Macro']) { + case 1: $result['Macro'] = (string) t("Macro"); break; + case 2: $result['Macro'] = (string) t("Normal"); break; + default: $result['Macro'] = (string) t("Unknown"); + } + $result['SelfTimer']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//2 + switch($result['SelfTimer']) { + case 0: $result['SelfTimer'] = (string) t("Off"); break; + default: $result['SelfTimer'] .= (string) t("/10s"); + } + $result['Quality']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//3 + switch($result['Quality']) { + case 2: $result['Quality'] = (string) t("Normal"); break; + case 3: $result['Quality'] = (string) t("Fine"); break; + case 5: $result['Quality'] = (string) t("Superfine"); break; + default: $result['Quality'] = (string) t("Unknown"); + } + $result['Flash']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//4 + switch($result['Flash']) { + case 0: $result['Flash'] = (string) t("Off"); break; + case 1: $result['Flash'] = (string) t("Auto"); break; + case 2: $result['Flash'] = (string) t("On"); break; + case 3: $result['Flash'] = (string) t("Red Eye Reduction"); break; + case 4: $result['Flash'] = (string) t("Slow Synchro"); break; + case 5: $result['Flash'] = (string) t("Auto + Red Eye Reduction"); break; + case 6: $result['Flash'] = (string) t("On + Red Eye Reduction"); break; + case 16: $result['Flash'] = (string) t("External Flash"); break; + default: $result['Flash'] = (string) t("Unknown"); + } + $result['DriveMode']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//5 + switch($result['DriveMode']) { + case 0: $result['DriveMode'] = (string) t("Single/Timer"); break; + case 1: $result['DriveMode'] = (string) t("Continuous"); break; + default: $result['DriveMode'] = (string) t("Unknown"); + } + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//6 + $result['FocusMode']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//7 + switch($result['FocusMode']) { + case 0: $result['FocusMode'] = (string) t("One-Shot"); break; + case 1: $result['FocusMode'] = (string) t("AI Servo"); break; + case 2: $result['FocusMode'] = (string) t("AI Focus"); break; + case 3: $result['FocusMode'] = (string) t("Manual Focus"); break; + case 4: $result['FocusMode'] = (string) t("Single"); break; + case 5: $result['FocusMode'] = (string) t("Continuous"); break; + case 6: $result['FocusMode'] = (string) t("Manual Focus"); break; + default: $result['FocusMode'] = (string) t("Unknown"); + } + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//8 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//9 + $result['ImageSize']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//10 + switch($result['ImageSize']) { + case 0: $result['ImageSize'] = (string) t("Large"); break; + case 1: $result['ImageSize'] = (string) t("Medium"); break; + case 2: $result['ImageSize'] = (string) t("Small"); break; + default: $result['ImageSize'] = (string) t("Unknown"); + } + $result['EasyShooting']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//11 + switch($result['EasyShooting']) { + case 0: $result['EasyShooting'] = (string) t("Full Auto"); break; + case 1: $result['EasyShooting'] = (string) t("Manual"); break; + case 2: $result['EasyShooting'] = (string) t("Landscape"); break; + case 3: $result['EasyShooting'] = (string) t("Fast Shutter"); break; + case 4: $result['EasyShooting'] = (string) t("Slow Shutter"); break; + case 5: $result['EasyShooting'] = (string) t("Night"); break; + case 6: $result['EasyShooting'] = (string) t("Black & White"); break; + case 7: $result['EasyShooting'] = (string) t("Sepia"); break; + case 8: $result['EasyShooting'] = (string) t("Portrait"); break; + case 9: $result['EasyShooting'] = (string) t("Sport"); break; + case 10: $result['EasyShooting'] = (string) t("Macro/Close-Up"); break; + case 11: $result['EasyShooting'] = (string) t("Pan Focus"); break; + default: $result['EasyShooting'] = (string) t("Unknown"); + } + $result['DigitalZoom']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//12 + switch($result['DigitalZoom']) { + case 0: + case 65535: $result['DigitalZoom'] = (string) t("None"); break; + case 1: $result['DigitalZoom'] = (string) t("2x"); break; + case 2: $result['DigitalZoom'] = (string) t("4x"); break; + default: $result['DigitalZoom'] = (string) t("Unknown"); + } + $result['Contrast']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//13 + switch($result['Contrast']) { + case 0: $result['Contrast'] = (string) t("Normal"); break; + case 1: $result['Contrast'] = (string) t("High"); break; + case 65535: $result['Contrast'] = (string) t("Low"); break; + default: $result['Contrast'] = (string) t("Unknown"); + } + $result['Saturation']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//14 + switch($result['Saturation']) { + case 0: $result['Saturation'] = (string) t("Normal"); break; + case 1: $result['Saturation'] = (string) t("High"); break; + case 65535: $result['Saturation'] = (string) t("Low"); break; + default: $result['Saturation'] = (string) t("Unknown"); + } + $result['Sharpness']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//15 + switch($result['Sharpness']) { + case 0: $result['Sharpness'] = (string) t("Normal"); break; + case 1: $result['Sharpness'] = (string) t("High"); break; + case 65535: $result['Sharpness'] = (string) t("Low"); break; + default: $result['Sharpness'] = (string) t("Unknown"); + } + $result['ISO']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//16 + switch($result['ISO']) { + case 32767: + case 0: $result['ISO'] = isset($exif['SubIFD']['ISOSpeedRatings']) + ? $exif['SubIFD']['ISOSpeedRatings'] : 'Unknown'; break; + case 15: $result['ISO'] = (string) t("Auto"); break; + case 16: $result['ISO'] = (string) t("50"); break; + case 17: $result['ISO'] = (string) t("100"); break; + case 18: $result['ISO'] = (string) t("200"); break; + case 19: $result['ISO'] = (string) t("400"); break; + default: $result['ISO'] = (string) t("Unknown"); + } + $result['MeteringMode']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//17 + switch($result['MeteringMode']) { + case 3: $result['MeteringMode'] = (string) t("Evaluative"); break; + case 4: $result['MeteringMode'] = (string) t("Partial"); break; + case 5: $result['MeteringMode'] = (string) t("Center-weighted"); break; + default: $result['MeteringMode'] = (string) t("Unknown"); + } + $result['FocusType']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//18 + switch($result['FocusType']) { + case 0: $result['FocusType'] = (string) t("Manual"); break; + case 1: $result['FocusType'] = (string) t("Auto"); break; + case 3: $result['FocusType'] = (string) t("Close-up (Macro)"); break; + case 8: $result['FocusType'] = (string) t("Locked (Pan Mode)"); break; + default: $result['FocusType'] = (string) t("Unknown"); + } + $result['AFPointSelected']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//19 + switch($result['AFPointSelected']) { + case 12288: $result['AFPointSelected'] = (string) t("Manual Focus"); break; + case 12289: $result['AFPointSelected'] = (string) t("Auto Selected"); break; + case 12290: $result['AFPointSelected'] = (string) t("Right"); break; + case 12291: $result['AFPointSelected'] = (string) t("Center"); break; + case 12292: $result['AFPointSelected'] = (string) t("Left"); break; + default: $result['AFPointSelected'] = (string) t("Unknown"); + } + $result['ExposureMode']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//20 + switch($result['ExposureMode']) { + case 0: $result['ExposureMode'] = (string) t("EasyShoot"); break; + case 1: $result['ExposureMode'] = (string) t("Program"); break; + case 2: $result['ExposureMode'] = (string) t("Tv"); break; + case 3: $result['ExposureMode'] = (string) t("Av"); break; + case 4: $result['ExposureMode'] = (string) t("Manual"); break; + case 5: $result['ExposureMode'] = (string) t("Auto-DEP"); break; + default: $result['ExposureMode'] = (string) t("Unknown"); + } + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//21 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//22 + $result['LongFocalLength']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//23 + $result['LongFocalLength'] .= " focal units"; + $result['ShortFocalLength']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//24 + $result['ShortFocalLength'] .= " focal units"; + $result['FocalUnits']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//25 + $result['FocalUnits'] .= " per mm"; + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//26 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//27 + $result['FlashActivity']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//28 + switch($result['FlashActivity']) { + case 0: $result['FlashActivity'] = (string) t("Flash Did Not Fire"); break; + case 1: $result['FlashActivity'] = (string) t("Flash Fired"); break; + default: $result['FlashActivity'] = (string) t("Unknown"); + } + $result['FlashDetails']=str_pad(base_convert(intel2Moto(substr($data,$place,4)), 16, 2), 16, "0", STR_PAD_LEFT);$place+=4;//29 + $flashDetails = array(); + if (substr($result['FlashDetails'], 1, 1) == 1) { $flashDetails[] = (string) t('External E-TTL'); } + if (substr($result['FlashDetails'], 2, 1) == 1) { $flashDetails[] = (string) t('Internal Flash'); } + if (substr($result['FlashDetails'], 4, 1) == 1) { $flashDetails[] = (string) t('FP sync used'); } + if (substr($result['FlashDetails'], 8, 1) == 1) { $flashDetails[] = (string) t('2nd(rear)-curtain sync used'); } + if (substr($result['FlashDetails'], 12, 1) == 1) { $flashDetails[] = (string) t('1st curtain sync'); } + $result['FlashDetails']=implode(",", $flashDetails); + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//30 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//31 + $anotherFocusMode=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//32 + if(strpos(strtoupper($exif['IFD0']['Model']), "G1") !== false) { + switch($anotherFocusMode) { + case 0: $result['FocusMode'] = (string) t("Single"); break; + case 1: $result['FocusMode'] = (string) t("Continuous"); break; + default: $result['FocusMode'] = (string) t("Unknown"); + } + } + + } else if($tag=="0004") { //second chunk + $result['Bytes']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//0 + if ($result['Bytes'] != strlen($data) / 2) return $result; //Bad chunk + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//1 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//2 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//3 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//4 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//5 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//6 + $result['WhiteBalance']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//7 + switch($result['WhiteBalance']) { + case 0: $result['WhiteBalance'] = (string) t("Auto"); break; + case 1: $result['WhiteBalance'] = (string) t("Sunny"); break; + case 2: $result['WhiteBalance'] = (string) t("Cloudy"); break; + case 3: $result['WhiteBalance'] = (string) t("Tungsten"); break; + case 4: $result['WhiteBalance'] = (string) t("Fluorescent"); break; + case 5: $result['WhiteBalance'] = (string) t("Flash"); break; + case 6: $result['WhiteBalance'] = (string) t("Custom"); break; + default: $result['WhiteBalance'] = (string) t("Unknown"); + } + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//8 + $result['SequenceNumber']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//9 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//10 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//11 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//12 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//13 + $result['AFPointUsed']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//14 + $afPointUsed = array(); + if ($result['AFPointUsed'] & 0x0001) $afPointUsed[] = (string) t("Right"); //bit 0 + if ($result['AFPointUsed'] & 0x0002) $afPointUsed[] = (string) t("Center"); //bit 1 + if ($result['AFPointUsed'] & 0x0004) $afPointUsed[] = (string) t("Left"); //bit 2 + if ($result['AFPointUsed'] & 0x0800) $afPointUsed[] = (string) t("12"); //bit 12 + if ($result['AFPointUsed'] & 0x1000) $afPointUsed[] = (string) t("13"); //bit 13 + if ($result['AFPointUsed'] & 0x2000) $afPointUsed[] = (string) t("14"); //bit 14 + if ($result['AFPointUsed'] & 0x4000) $afPointUsed[] = (string) t("15"); //bit 15 + $result['AFPointUsed'] = implode(",", $afPointUsed); + $result['FlashBias']=intel2Moto(substr($data,$place,4));$place+=4;//15 + switch($result['FlashBias']) { + case 'ffc0': $result['FlashBias'] = "-2 EV"; break; + case 'ffcc': $result['FlashBias'] = "-1.67 EV"; break; + case 'ffd0': $result['FlashBias'] = "-1.5 EV"; break; + case 'ffd4': $result['FlashBias'] = "-1.33 EV"; break; + case 'ffe0': $result['FlashBias'] = "-1 EV"; break; + case 'ffec': $result['FlashBias'] = "-0.67 EV"; break; + case 'fff0': $result['FlashBias'] = "-0.5 EV"; break; + case 'fff4': $result['FlashBias'] = "-0.33 EV"; break; + case '0000': $result['FlashBias'] = "0 EV"; break; + case '000c': $result['FlashBias'] = "0.33 EV"; break; + case '0010': $result['FlashBias'] = "0.5 EV"; break; + case '0014': $result['FlashBias'] = "0.67 EV"; break; + case '0020': $result['FlashBias'] = "1 EV"; break; + case '002c': $result['FlashBias'] = "1.33 EV"; break; + case '0030': $result['FlashBias'] = "1.5 EV"; break; + case '0034': $result['FlashBias'] = "1.67 EV"; break; + case '0040': $result['FlashBias'] = "2 EV"; break; + default: $result['FlashBias'] = (string) t("Unknown"); + } + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//16 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//17 + $result['Unknown']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//18 + $result['SubjectDistance']=hexdec(intel2Moto(substr($data,$place,4)));$place+=4;//19 + $result['SubjectDistance'] .= "/100 m"; + + } else if($tag=="0008") { //image number + if($intel==1) $data = intel2Moto($data); + $data=hexdec($data); + $result = round($data/10000)."-".$data%10000; + } else if($tag=="000c") { //camera serial number + if($intel==1) $data = intel2Moto($data); + $data=hexdec($data); + $result = "#".bin2hex(substr($data,0,16)).substr($data,16,16); + } + + } else if($type=="UNDEFINED") { + + + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + + +//================= +// Cannon Special data section +// Useful: http://www.burren.cx/david/canon.html +// http://www.burren.cx/david/canon.html +// http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html +//==================================================================== +function parseCanon($block,&$result,$seek, $globalOffset) { + $place = 0; //current place + + if($result['Endien']=="Intel") $intel=1; + else $intel=0; + + $model = $result['IFD0']['Model']; + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = hexdec($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Canon_tag($tag); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + if($bytesofdata<=0) { + return; //if this value is 0 or less then we have read all the tags we can + } + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $v = fseek($seek,$globalOffset+hexdec($value)); //offsets are from TIFF header which is 12 bytes from the start of the file + if (isset($GLOBALS['exiferFileSize'])) { + $exiferFileSize = $GLOBALS['exiferFileSize']; + } else { + $exiferFileSize = 0; + } + if($v==0 && $bytesofdata < $exiferFileSize) { + $data = fread($seek, $bytesofdata); + } else if($v==-1) { + $result['Errors'] = $result['Errors']++; + $data = ''; + } else { + $data = ''; + } + } + $result['SubIFD']['MakerNote'][$tag_name] = ''; // insure the index exists + $formated_data = formatCanonData($type,$tag,$intel,$data,$result,$result['SubIFD']['MakerNote'][$tag_name]); + + if($result['VerboseOutput']==1) { + //$result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + //$result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } +} + + +?>
\ No newline at end of file diff --git a/modules/exif/lib/makers/fujifilm.php b/modules/exif/lib/makers/fujifilm.php new file mode 100644 index 0000000..a1f2f41 --- /dev/null +++ b/modules/exif/lib/makers/fujifilm.php @@ -0,0 +1,247 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright � 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Fujifilm_tag($tag) { + + switch($tag) { + case "0000": $tag = "Version";break; + case "1000": $tag = "Quality";break; + case "1001": $tag = "Sharpness";break; + case "1002": $tag = "WhiteBalance";break; + case "1003": $tag = "Color";break; + case "1004": $tag = "Tone";break; + case "1010": $tag = "FlashMode";break; + case "1011": $tag = "FlashStrength";break; + case "1020": $tag = "Macro";break; + case "1021": $tag = "FocusMode";break; + case "1030": $tag = "SlowSync";break; + case "1031": $tag = "PictureMode";break; + case "1100": $tag = "ContinuousTakingBracket";break; + case "1200": $tag = "Unknown";break; + case "1300": $tag = "BlurWarning";break; + case "1301": $tag = "FocusWarning";break; + case "1302": $tag = "AEWarning";break; + + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatFujifilmData($type,$tag,$intel,$data) { + + if($type=="ASCII") { + + + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + $data = unRational($data,$type,$intel); + + if($tag=="1011") { //FlashStrength + $data=$data." EV"; + } + + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data =rational($data,$type,$intel); + + if($tag=="1001") { //Sharpness + if($data == 1) $data = (string) t("Soft"); + else if($data == 2) $data = (string) t("Soft"); + else if($data == 3) $data = (string) t("Normal"); + else if($data == 4) $data = (string) t("Hard"); + else if($data == 5) $data = (string) t("Hard"); + else $data = (string) t("Unknown").": ".$data; + } + if($tag=="1002") { //WhiteBalance + if($data == 0) $data = (string) t("Auto"); + else if($data == 256) $data = (string) t("Daylight"); + else if($data == 512) $data = (string) t("Cloudy"); + else if($data == 768) $data = (string) t("DaylightColor-fluorescence"); + else if($data == 769) $data = (string) t("DaywhiteColor-fluorescence"); + else if($data == 770) $data = (string) t("White-fluorescence"); + else if($data == 1024) $data = (string) t("Incandescence"); + else if($data == 3840) $data = (string) t("Custom"); + else $data = (string) t("Unknown").": ".$data; + } + if($tag=="1003") { //Color + if($data == 0) $data = (string) t("Chroma Saturation Normal(STD)"); + else if($data == 256) $data = (string) t("Chroma Saturation High"); + else if($data == 512) $data = (string) t("Chroma Saturation Low(ORG)"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1004") { //Tone + if($data == 0) $data = (string) t("Contrast Normal(STD)"); + else if($data == 256) $data = (string) t("Contrast High(HARD)"); + else if($data == 512) $data = (string) t("Contrast Low(ORG)"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1010") { //FlashMode + if($data == 0) $data = (string) t("Auto"); + else if($data == 1) $data = (string) t("On"); + else if($data == 2) $data = (string) t("Off"); + else if($data == 3) $data = (string) t("Red-Eye Reduction"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1020") { //Macro + if($data == 0) $data = (string) t("Off"); + else if($data == 1) $data = (string) t("On"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1021") { //FocusMode + if($data == 0) $data = (string) t("Auto"); + else if($data == 1) $data = (string) t("Manual"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1030") { //SlowSync + if($data == 0) $data = (string) t("Off"); + else if($data == 1) $data = (string) t("On"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1031") { //PictureMode + if($data == 0) $data = (string) t("Auto"); + else if($data == 1) $data = (string) t("Portrait"); + else if($data == 2) $data = (string) t("Landscape"); + else if($data == 4) $data = (string) t("Sports"); + else if($data == 5) $data = (string) t("Night"); + else if($data == 6) $data = (string) t("Program AE"); + else if($data == 256) $data = (string) t("Aperture Priority AE"); + else if($data == 512) $data = (string) t("Shutter Priority"); + else if($data == 768) $data = (string) t("Manual Exposure"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1100") { //ContinuousTakingBracket + if($data == 0) $data = (string) t("Off"); + else if($data == 1) $data = (string) t("On"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1300") { //BlurWarning + if($data == 0) $data = (string) t("No Warning"); + else if($data == 1) $data = (string) t("Warning"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1301") { //FocusWarning + if($data == 0) $data = (string) t("Auto Focus Good"); + else if($data == 1) $data = (string) t("Out of Focus"); + else $data = (string) t("Unknown: ").$data; + } + if($tag=="1302") { //AEWarning + if($data == 0) $data = (string) t("AE Good"); + else if($data == 1) $data = (string) t("Over Exposure"); + else $data = (string) t("Unknown: ").$data; + } + } else if($type=="UNDEFINED") { + + + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + + +//================= +// Fujifilm Special data section +//==================================================================== +function parseFujifilm($block,&$result) { + + //if($result['Endien']=="Intel") $intel=1; + //else $intel=0; + $intel=1; + + $model = $result['IFD0']['Model']; + + $place=8; //current place + $offset=8; + + + $num = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['Offset'] = hexdec($num); + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = hexdec($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Fujifilm_tag($tag); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + + + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $data = substr($block,hexdec($value)-$offset,$bytesofdata*2); + } + $formated_data = formatFujifilmData($type,$tag,$intel,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } +} + + +?>
\ No newline at end of file diff --git a/modules/exif/lib/makers/gps.php b/modules/exif/lib/makers/gps.php new file mode 100644 index 0000000..462aae6 --- /dev/null +++ b/modules/exif/lib/makers/gps.php @@ -0,0 +1,218 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright © 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + + +//================= +// Looks up the name of the tag +//==================================================================== +function lookup_GPS_tag($tag) { + + switch($tag) { + case "0000": $tag = "Version";break; + case "0001": $tag = "Latitude Reference";break; //north or south + case "0002": $tag = "Latitude";break; //dd mm.mm or dd mm ss + case "0003": $tag = "Longitude Reference";break; //east or west + case "0004": $tag = "Longitude";break; //dd mm.mm or dd mm ss + case "0005": $tag = "Altitude Reference";break; //sea level or below sea level + case "0006": $tag = "Altitude";break; //positive rational number + case "0007": $tag = "Time";break; //three positive rational numbers + case "0008": $tag = "Satellite";break; //text string up to 999 bytes long + case "0009": $tag = "ReceiveStatus";break; //in progress or interop + case "000a": $tag = "MeasurementMode";break; //2D or 3D + case "000b": $tag = "MeasurementPrecision";break; //positive rational number + case "000c": $tag = "SpeedUnit";break; //KPH, MPH, knots + case "000d": $tag = "ReceiverSpeed";break; //positive rational number + case "000e": $tag = "MovementDirectionRef";break; //true or magnetic north + case "000f": $tag = "MovementDirection";break; //positive rational number + case "0010": $tag = "ImageDirectionRef";break; //true or magnetic north + case "0011": $tag = "ImageDirection";break; //positive rational number + case "0012": $tag = "GeodeticSurveyData";break; //text string up to 999 bytes long + case "0013": $tag = "DestLatitudeRef";break; //north or south + case "0014": $tag = "DestinationLatitude";break; //three positive rational numbers + case "0015": $tag = "DestLongitudeRef";break; //east or west + case "0016": $tag = "DestinationLongitude";break; //three positive rational numbers + case "0017": $tag = "DestBearingRef";break; //true or magnetic north + case "0018": $tag = "DestinationBearing";break; //positive rational number + case "0019": $tag = "DestDistanceRef";break; //km, miles, knots + case "001a": $tag = "DestinationDistance";break; //positive rational number + case "001b": $tag = "ProcessingMethod";break; + case "001c": $tag = "AreaInformation";break; + case "001d": $tag = "Datestamp";break; //text string 10 bytes long + case "001e": $tag = "DifferentialCorrection";break; //integer in range 0-65535 + + + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatGPSData($type,$tag,$intel,$data) { + + if($type=="ASCII") { + if($tag=="0001" || $tag=="0003"){ // Latitude Reference, Longitude Reference + $data = ($data{1} == $data{2} && $data{1} == $data{3}) ? $data{0} : $data; + } + + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + if($tag=="0002" || $tag=="0004" || $tag=='0007') { //Latitude, Longitude, Time + $datum = array(); + for ($i=0;$i<strlen($data);$i=$i+8) { + array_push($datum,substr($data, $i, 8)); + } + $hour = unRational($datum[0],$type,$intel); + $minutes = unRational($datum[1],$type,$intel); + $seconds = unRational($datum[2],$type,$intel); + if($tag=="0007") { //Time + $data = $hour.":".$minutes.":".$seconds; + } else { + $data = $hour+$minutes/60+$seconds/3600; + } + } else { + $data = unRational($data,$type,$intel); + + if($tag=="0006"){ + $data .= 'm'; + } + } + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = rational($data,$type,$intel); + + + } else if($type=="UNDEFINED") { + + + + } else if($type=="UBYTE") { + $data = bin2hex($data); + if($intel==1) $num = intel2Moto($data); + + + if($tag=="0000") { // VersionID + $data = hexdec(substr($data,0,2)) . + ".". hexdec(substr($data,2,2)) . + ".". hexdec(substr($data,4,2)) . + ".". hexdec(substr($data,6,2)); + + } else if($tag=="0005"){ // Altitude Reference + if($data == "00000000"){ $data = '+'; } + else if($data == "01000000"){ $data = '-'; } + } + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + +//================= +// GPS Special data section +// Useful websites +// http://drewnoakes.com/code/exif/sampleOutput.html +// http://www.geosnapper.com +//==================================================================== +function parseGPS($block,&$result,$offset,$seek, $globalOffset) { + + if($result['Endien']=="Intel") $intel=1; + else $intel=0; + + $v = fseek($seek,$globalOffset+$offset); //offsets are from TIFF header which is 12 bytes from the start of the file + if($v==-1) { + $result['Errors'] = $result['Errors']++; + } + + $num = bin2hex(fread( $seek, 2 )); + if($intel==1) $num = intel2Moto($num); + $num=hexdec($num); + $result['GPS']['NumTags'] = $num; + + if ($num == 0) { + return; + } + + $block = fread( $seek, $num*12 ); + $place = 0; + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<$num;$i++) { + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_GPS_tag($tag); + + //2 byte datatype + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte number of elements + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value or pointer to value if larger than 4 bytes + $value = substr($block,$place,4);$place+=4; + if($bytesofdata<=4) { + $data = $value; + } else { + if (strpos('unknown',$tag_name) !== false || $bytesofdata > 1024) { + $result['Errors'] = $result['Errors']++; + $data = ''; + $type = 'ASCII'; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $v = fseek($seek,$globalOffset+hexdec($value)); //offsets are from TIFF header which is 12 bytes from the start of the file + if($v==0) { + $data = fread($seek, $bytesofdata); + } else { + $result['Errors'] = $result['Errors']++; + $data = ''; + $type = 'ASCII'; + } + } + } + if($result['VerboseOutput']==1) { + $result['GPS'][$tag_name] = formatGPSData($type,$tag,$intel,$data); + $result['GPS'][$tag_name."_Verbose"]['RawData'] = bin2hex($data); + $result['GPS'][$tag_name."_Verbose"]['Type'] = $type; + $result['GPS'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['GPS'][$tag_name] = formatGPSData($type,$tag,$intel,$data); + } + } +} + + +?> diff --git a/modules/exif/lib/makers/nikon.php b/modules/exif/lib/makers/nikon.php new file mode 100644 index 0000000..d2fff9a --- /dev/null +++ b/modules/exif/lib/makers/nikon.php @@ -0,0 +1,411 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright � 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Nikon_tag($tag,$model) { + + if($model==0) { + switch($tag) { + case "0003": $tag = "Quality";break; + case "0004": $tag = "ColorMode";break; + case "0005": $tag = "ImageAdjustment";break; + case "0006": $tag = "CCDSensitivity";break; + case "0007": $tag = "WhiteBalance";break; + case "0008": $tag = "Focus";break; + case "0009": $tag = "Unknown2";break; + case "000a": $tag = "DigitalZoom";break; + case "000b": $tag = (string) t("Converter");break; + + default: $tag = "unknown:".$tag;break; + } + } else if($model==1) { + switch($tag) { + case "0002": $tag = "ISOSetting";break; + case "0003": $tag = "ColorMode";break; + case "0004": $tag = "Quality";break; + case "0005": $tag = "Whitebalance";break; + case "0006": $tag = "ImageSharpening";break; + case "0007": $tag = "FocusMode";break; + case "0008": $tag = "FlashSetting";break; + case "0009": $tag = "FlashMode";break; + case "000b": $tag = "WhiteBalanceFine";break; + case "000c": $tag = "WB_RBLevels";break; + case "000d": $tag = "ProgramShift";break; + case "000e": $tag = "ExposureDifference";break; + case "000f": $tag = "ISOSelection";break; + case "0010": $tag = "DataDump";break; + case "0011": $tag = "NikonPreview";break; + case "0012": $tag = "FlashExposureComp";break; + case "0013": $tag = "ISOSetting2";break; + case "0014": $tag = "ColorBalanceA";break; + case "0016": $tag = "ImageBoundary";break; + case "0017": $tag = "FlashExposureComp";break; + case "0018": $tag = "FlashExposureBracketValue";break; + case "0019": $tag = "ExposureBracketValue";break; + case "001a": $tag = "ImageProcessing";break; + case "001b": $tag = "CropHiSpeed";break; + case "001c": $tag = "ExposureTuning";break; + case "001d": $tag = "SerialNumber";break; + case "001e": $tag = "ColorSpace";break; + case "001f": $tag = "VRInfo";break; + case "0020": $tag = "ImageAuthentication";break; + case "0022": $tag = "ActiveD-Lighting";break; + case "0023": $tag = "PictureControl";break; + case "0024": $tag = "WorldTime";break; + case "0025": $tag = "ISOInfo";break; + case "002a": $tag = "VignetteControl";break; + case "002b": $tag = "DistortInfo";break; + case "0080": $tag = "ImageAdjustment";break; + case "0081": $tag = "ToneCompensation";break; + case "0082": $tag = "Adapter";break; + case "0083": $tag = "LensType";break; + case "0084": $tag = "LensInfo";break; + case "0085": $tag = "ManualFocusDistance";break; + case "0086": $tag = "DigitalZoom";break; + case "0087": $tag = "FlashUsed";break; + case "0088": $tag = "AFFocusPosition";break; + case "0089": $tag = "ShootingMode";break; + case "008b": $tag = "LensFStops";break; + case "008c": $tag = "ContrastCurve";break; + case "008d": $tag = "ColorMode";break; + case "0090": $tag = "LightType";break; + case "0092": $tag = "HueAdjustment";break; + case "0093": $tag = "NEFCompression";break; + case "0094": $tag = "Saturation";break; + case "0095": $tag = "NoiseReduction";break; + case "009a": $tag = "SensorPixelSize";break; + + default: $tag = "unknown:".$tag;break; + } + } + + return $tag; +} + + +//================= +// Formats Data for the data type +//==================================================================== +function formatNikonData($type,$tag,$intel,$model,$data) { + switch ($type) { + case "ASCII": + break; // do nothing! + case "URATIONAL": + case"SRATIONAL": + switch ($tag) { + case '0084': // LensInfo + $minFL = unRational(substr($data,0,8),$type,$intel); + $maxFL = unRational(substr($data,8,8),$type,$intel); + $minSP = unRational(substr($data,16,8),$type,$intel); + $maxSP = unRational(substr($data,24,8),$type,$intel); + if ($minFL == $maxFL) { + $data = sprintf('%0.0f f/%0.0f',$minFL,$minSP); + } elseif ($minSP == $maxSP) { + $data = sprintf('%0.0f-%0.0fmm f/%0.1f',$minFL,$maxFL,$minSP); + } else { + $data = sprintf('%0.0f-%0.0fmm f/%0.1f-%0.1f',$minFL,$maxFL,$minSP,$maxSP); + } + break; + case "0085": + if ($model==1) $data=unRational($data,$type,$intel)." m"; //ManualFocusDistance + break; + case "0086": + if ($model==1) $data=unRational($data,$type,$intel)."x"; //DigitalZoom + break; + case "000a": + if ($model==0) $data=unRational($data,$type,$intel)."x"; //DigitalZoom + break; + default: + $data=unRational($data,$type,$intel); + break; + } + break; + case "USHORT": + case $type=="SSHORT": + case $type=="ULONG": + case $type=="SLONG": + case $type=="FLOAT": + case $type=="DOUBLE": + $data = rational($data,$type,$intel); + switch ($tag) { + case "0003": + if ($model==0) { //Quality + switch ($data) { + case 1: $data = (string) t("VGA Basic"); break; + case 2: $data = (string) t("VGA Normal"); break; + case 3: $data = (string) t("VGA Fine"); break; + case 4: $data = (string) t("SXGA Basic"); break; + case 5: $data = (string) t("SXGA Normal"); break; + case 6: $data = (string) t("SXGA Fine"); break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "0004": + if ($model==0) { //Color + switch ($data) { + case 1: $data = (string) t("Color"); break; + case 2: $data = (string) t("Monochrome"); break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "0005": + if ($model==0) { //Image Adjustment + switch ($data) { + case 0: $data = (string) t("Normal"); break; + case 1: $data = (string) t("Bright+"); break; + case 2: $data = (string) t("Bright-"); break; + case 3: $data = (string) t("Contrast+"); break; + case 4: $data = (string) t("Contrast-"); break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "0006": + if ($model==0) { //CCD Sensitivity + switch($data) { + case 0: $data = "ISO-80"; break; + case 2: $data = "ISO-160"; break; + case 4: $data = "ISO-320"; break; + case 5: $data = "ISO-100"; break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "0007": + if ($model==0) { //White Balance + switch ($data) { + case 0: $data = (string) t("Auto"); break; + case 1: $data = (string) t("Preset"); break; + case 2: $data = (string) t("Daylight"); break; + case 3: $data = (string) t("Incandescence"); break; + case 4: $data = (string) t("Fluorescence"); break; + case 5: $data = (string) t("Cloudy"); break; + case 6: $data = (string) t("SpeedLight"); break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "000b": + if ($model==0) { //Converter + switch ($data) { + case 0: $data = (string) t("None"); break; + case 1: $data = (string) t("Fisheye"); break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + } + break; + case "UNDEFINED": + switch ($tag) { + case "0001": + if ($model==1) $data=$data/100; break; //Unknown (Version?) + break; + case "0088": + if ($model==1) { //AF Focus Position + $temp = (string) t("Center"); + $data = bin2hex($data); + $data = str_replace("01","Top",$data); + $data = str_replace("02","Bottom",$data); + $data = str_replace("03","Left",$data); + $data = str_replace("04","Right",$data); + $data = str_replace("00","",$data); + if(strlen($data)==0) $data = $temp; + } + break; + } + break; + default: + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + switch ($tag) { + case "0083": + if ($model==1) { //Lens Type + $data = hexdec(substr($data,0,2)); + switch ($data) { + case 0: $data = (string) t("AF non D"); break; + case 1: $data = (string) t("Manual"); break; + case 2: $data = "AF-D or AF-S"; break; + case 6: $data = "AF-D G"; break; + case 10: $data = "AF-D VR"; break; + case 14: $data = "AF-D G VR"; break; + default: $data = (string) t("Unknown").": ".$data; break; + } + } + break; + case "0087": + if ($model==1) { //Flash type + $data = hexdec(substr($data,0,2)); + if($data == 0) $data = (string) t("Did Not Fire"); + else if($data == 4) $data = (string) t("Unknown"); + else if($data == 7) $data = (string) t("External"); + else if($data == 9) $data = (string) t("On Camera"); + else $data = (string) t("Unknown").": ".$data; + } + break; + } + break; + } + return $data; +} + + +//================= +// Nikon Special data section +//==================================================================== +function parseNikon($block,&$result) { + + if($result['Endien']=="Intel") $intel=1; + else $intel=0; + + $model = $result['IFD0']['Model']; + + //these 6 models start with "Nikon". Other models dont. + if($model=="E700\0" || $model=="E800\0" || $model=="E900\0" || $model=="E900S\0" || $model=="E910\0" || $model=="E950\0") { + $place=8; //current place + $model = 0; + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = hexdec($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Nikon_tag($tag, $model); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + + //if tag is 0002 then its the ASCII value which we know is at 140 so calc offset + //THIS HACK ONLY WORKS WITH EARLY NIKON MODELS + if($tag=="0002") $offset = hexdec($value)-140; + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $data = substr($block,hexdec($value)-$offset,$bytesofdata*2); + } + $formated_data = formatNikonData($type,$tag,$intel,$model,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } + + } else { + $place=0;//current place + $model = 1; + + $nikon = substr($block,$place,8);$place+=8; + $endien = substr($block,$place,4);$place+=4; + + //2 bytes of 0x002a + $tag = bin2hex(substr($block,$place,2));$place+=2; + + //Then 4 bytes of offset to IFD0 (usually 8 which includes all 8 bytes of TIFF header) + $offset = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $offset = intel2Moto($offset); + if(hexdec($offset)>8) $place+=$offset-8; + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Nikon_tag($tag, $model); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $data = substr($block,hexdec($value)+hexdec($offset)+2,$bytesofdata); + } + $formated_data = formatNikonData($type,$tag,$intel,$model,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } + + } +} + + +?>
\ No newline at end of file diff --git a/modules/exif/lib/makers/olympus.php b/modules/exif/lib/makers/olympus.php new file mode 100644 index 0000000..3382fc7 --- /dev/null +++ b/modules/exif/lib/makers/olympus.php @@ -0,0 +1,189 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright � 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Olympus_tag($tag) { + switch($tag) { + case "0200": $tag = "SpecialMode";break; + case "0201": $tag = "JpegQual";break; + case "0202": $tag = "Macro";break; + case "0203": $tag = "Unknown1";break; + case "0204": $tag = "DigiZoom";break; + case "0205": $tag = "Unknown2";break; + case "0206": $tag = "Unknown3";break; + case "0207": $tag = "SoftwareRelease";break; + case "0208": $tag = "PictInfo";break; + case "0209": $tag = "CameraID";break; + case "0f00": $tag = "DataDump";break; + + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatOlympusData($type,$tag,$intel,$data) { + if($type=="ASCII") { + + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + $data = unRational($data,$type,$intel); + if($intel==1) $data = intel2Moto($data); + + if($tag=="0204") { //DigitalZoom + $data=$data."x"; + } + if($tag=="0205") { //Unknown2 + + } + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = rational($data,$type,$intel); + + if($tag=="0201") { //JPEGQuality + if($data == 1) $data = "SQ"; + else if($data == 2) $data = "HQ"; + else if($data == 3) $data = "SHQ"; + else $data = (string) t("Unknown").": ".$data; + } + if($tag=="0202") { //Macro + if($data == 0) $data = "Normal"; + else if($data == 1) $data = "Macro"; + else $data = (string) t("Unknown").": ".$data; + } + } else if($type=="UNDEFINED") { + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + + +//============================================================================== +// Olympus Special data section +// - Updated by Zenphoto for new header tag in E-410/E-510/E-3 cameras. 2/24/2008 +//============================================================================== +function parseOlympus($block, &$result, $seek, $globalOffset) { + + if($result['Endien']=="Intel") $intel = 1; + else $intel = 0; + + $model = $result['IFD0']['Model']; + + // New header for new DSLRs - Check for it because the + // number of bytes that count the IFD fields differ in each case. + // Fixed by Zenphoto 2/24/08 + $new = false; + if (substr($block, 0, 8) == "OLYMPUS\x00") { + $new = true; + } else if (substr($block, 0, 7) == "OLYMP\x00\x01" + || substr($block, 0, 7) == "OLYMP\x00\x02") { + $new = false; + } else { + // Header does not match known Olympus headers. + // This is not a valid OLYMPUS Makernote. + return false; + } + + // Offset of IFD entry after Olympus header. + $place = 8; + $offset = 8; + + // Get number of tags (1 or 2 bytes, depending on New or Old makernote) + $countfieldbits = $new ? 1 : 2; + // New makernote repeats 1-byte value twice, so increment $place by 2 in either case. + $num = bin2hex(substr($block, $place, $countfieldbits)); $place += 2; + if ($intel == 1) $num = intel2Moto($num); + $ntags = hexdec($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = $ntags; + + //loop thru all tags Each field is 12 bytes + for($i=0; $i < $ntags; $i++) { + //2 byte tag + $tag = bin2hex(substr($block, $place,2)); + $place += 2; + if ($intel == 1) $tag = intel2Moto($tag); + $tag_name = lookup_Olympus_tag($tag); + + //2 byte type + $type = bin2hex(substr($block, $place,2)); + $place += 2; + if ($intel == 1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block, $place,4)); + $place+=4; + if ($intel == 1) $count = intel2Moto($count); + $bytesofdata = $size * hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block, $place,4); + $place += 4; + + + if ($bytesofdata <= 4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $v = fseek($seek,$globalOffset+hexdec($value)); //offsets are from TIFF header which is 12 bytes from the start of the file + if(isset($GLOBALS['exiferFileSize']) && $v == 0 && $bytesofdata < $GLOBALS['exiferFileSize']) { + $data = fread($seek, $bytesofdata); + } else { + $result['Errors'] = $result['Errors']++; + $data = ''; + } + } + $formated_data = formatOlympusData($type,$tag,$intel,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } +} + + +?>
\ No newline at end of file diff --git a/modules/exif/lib/makers/panasonic.php b/modules/exif/lib/makers/panasonic.php new file mode 100644 index 0000000..47a0599 --- /dev/null +++ b/modules/exif/lib/makers/panasonic.php @@ -0,0 +1,292 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright � 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Panasonic_tag($tag) { + + switch($tag) { + case "0001": $tag = "Quality";break; + case "0002": $tag = "FirmwareVersion";break; + case "0003": $tag = "WhiteBalance";break; + case "0007": $tag = "FocusMode";break; + case "000f": $tag = "AFMode";break; + case "001a": $tag = "ImageStabilizer";break; + case "001c": $tag = "MacroMode";break; + case "001f": $tag = "ShootingMode";break; + case "0020": $tag = "Audio";break; + case "0021": $tag = "DataDump";break; + case "0023": $tag = "WhiteBalanceBias";break; + case "0024": $tag = "FlashBias";break; + case "0025": $tag = "SerialNumber";break; + case "0028": $tag = "ColourEffect";break; + case "002a": $tag = "BurstMode";break; + case "002b": $tag = "SequenceNumber";break; + case "002c": $tag = "Contrast";break; + case "002d": $tag = "NoiseReduction";break; + case "002e": $tag = "SelfTimer";break; + case "0030": $tag = "Rotation";break; + case "0032": $tag = "ColorMode";break; + case "0036": $tag = "TravelDay";break; + + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatPanasonicData($type,$tag,$intel,$data) { + + if($type=="ASCII") { + + } else if($type=="UBYTE" || $type=="SBYTE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + $data=hexdec($data); + + if($tag=="000f") { //AFMode + if($data == 256) $data = "9-area-focusing"; + else if($data == 16) $data = "1-area-focusing"; + else if($data == 4096) $data = (string) t("3-area-focusing (High speed)"); + else if($data == 4112) $data = (string) t("1-area-focusing (High speed)"); + else if($data == 16) $data = (string) t("1-area-focusing"); + else if($data == 1) $data = (string) t("Spot-focusing"); + else $data = "Unknown (".$data.")"; + } + + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + $data = unRational($data,$type,$intel); + + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = rational($data,$type,$intel); + + if($tag=="0001") { //Image Quality + if($data == 2) $data = (string) t("High"); + else if($data == 3) $data = (string) t("Standard"); + else if($data == 6) $data = (string) t("Very High"); + else if($data == 7) $data = (string) t("RAW"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0003") { //White Balance + if($data == 1) $data = (string) t("Auto"); + else if($data == 2) $data = (string) t("Daylight"); + else if($data == 3) $data = (string) t("Cloudy"); + else if($data == 4) $data = (string) t("Halogen"); + else if($data == 5) $data = (string) t("Manual"); + else if($data == 8) $data = (string) t("Flash"); + else if($data == 10) $data = (string) t("Black and White"); + else if($data == 11) $data = (string) t("Manual"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0007") { //Focus Mode + if($data == 1) $data = (string) t("Auto"); + else if($data == 2) $data = (string) t("Manual"); + else if($data == 4) $data = (string) t("Auto, Focus button"); + else if($data == 5) $data = (string) t("Auto, Continuous"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="001a") { //Image Stabilizer + if($data == 2) $data = (string) t("Mode 1"); + else if($data == 3) $data = (string) t("Off"); + else if($data == 4) $data = (string) t("Mode 2"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="001c") { //Macro mode + if($data == 1) $data = (string) t("On"); + else if($data == 2) $data = (string) t("Off"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="001f") { //Shooting Mode + if($data == 1) $data = (string) t("Normal"); + else if($data == 2) $data = (string) t("Portrait"); + else if($data == 3) $data = (string) t("Scenery"); + else if($data == 4) $data = (string) t("Sports"); + else if($data == 5) $data = (string) t("Night Portrait"); + else if($data == 6) $data = (string) t("Program"); + else if($data == 7) $data = (string) t("Aperture Priority"); + else if($data == 8) $data = (string) t("Shutter Priority"); + else if($data == 9) $data = (string) t("Macro"); + else if($data == 11) $data = (string) t("Manual"); + else if($data == 13) $data = (string) t("Panning"); + else if($data == 14) $data = (string) t("Simple"); + else if($data == 18) $data = (string) t("Fireworks"); + else if($data == 19) $data = (string) t("Party"); + else if($data == 20) $data = (string) t("Snow"); + else if($data == 21) $data = (string) t("Night Scenery"); + else if($data == 22) $data = (string) t("Food"); + else if($data == 23) $data = (string) t("Baby"); + else if($data == 27) $data = (string) t("High Sensitivity"); + else if($data == 29) $data = (string) t("Underwater"); + else if($data == 33) $data = (string) t("Pet"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0020") { //Audio + if($data == 1) $data = (string) t("Yes"); + else if($data == 2) $data = (string) t("No"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0023") { //White Balance Bias + $data=$data." EV"; + } + if($tag=="0024") { //Flash Bias + $data = $data; + } + if($tag=="0028") { //Colour Effect + if($data == 1) $data = (string) t("Off"); + else if($data == 2) $data = (string) t("Warm"); + else if($data == 3) $data = (string) t("Cool"); + else if($data == 4) $data = (string) t("Black and White"); + else if($data == 5) $data = (string) t("Sepia"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="002a") { //Burst Mode + if($data == 0) $data = (string) t("Off"); + else if($data == 1) $data = (string) t("Low/High Quality"); + else if($data == 2) $data = (string) t("Infinite"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="002c") { //Contrast + if($data == 0) $data = (string) t("Standard"); + else if($data == 1) $data = (string) t("Low"); + else if($data == 2) $data = (string) t("High"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="002d") { //Noise Reduction + if($data == 0) $data = (string) t("Standard"); + else if($data == 1) $data = (string) t("Low"); + else if($data == 2) $data = (string) t("High"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="002e") { //Self Timer + if($data == 1) $data = (string) t("Off"); + else if($data == 2) $data = (string) t("10s"); + else if($data == 3) $data = (string) t("2s"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0030") { //Rotation + if($data == 1) $data = (string) t("Horizontal (normal)"); + else if($data == 6) $data = (string) t("Rotate 90 CW"); + else if($data == 8) $data = (string) t("Rotate 270 CW"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0032") { //Color Mode + if($data == 0) $data = (string) t("Normal"); + else if($data == 1) $data = (string) t("Natural"); + else $data = (string) t("Unknown")." (".$data.")"; + } + if($tag=="0036") { //Travel Day + $data=$data; + } + } else if($type=="UNDEFINED") { + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + + +//================= +// Panasonic Special data section +//==================================================================== +function parsePanasonic($block,&$result) { + + //if($result['Endien']=="Intel") $intel=1; + //else $intel=0; + $intel=1; + + $model = $result['IFD0']['Model']; + + $place=8; //current place + $offset=8; + + + $num = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['Offset'] = hexdec($num); + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = hexdec($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Panasonic_tag($tag); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + + + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $data = substr($block,hexdec($value)-$offset,$bytesofdata*2); + } + $formated_data = formatPanasonicData($type,$tag,$intel,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } +} + +?>
\ No newline at end of file diff --git a/modules/exif/lib/makers/sanyo.php b/modules/exif/lib/makers/sanyo.php new file mode 100644 index 0000000..3eef201 --- /dev/null +++ b/modules/exif/lib/makers/sanyo.php @@ -0,0 +1,158 @@ +<?php defined("SYSPATH") or die("No direct script access."); +//================================================================================================ +//================================================================================================ +/* + Exifer + Extracts EXIF information from digital photos. + + Copyright � 2003 Jake Olefsky + http://www.offsky.com/software/exif/index.php + jake@olefsky.com + + Please see exif.php for the complete information about this software. + + ------------ + + 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. http://www.gnu.org/copyleft/gpl.html +*/ +//================================================================================================ +//================================================================================================ +//================================================================================================ + + + +//================= +// Looks up the name of the tag for the MakerNote (Depends on Manufacturer) +//==================================================================== +function lookup_Sanyo_tag($tag) { + + switch($tag) { + case "0200": $tag = "SpecialMode";break; + case "0201": $tag = "Quality";break; + case "0202": $tag = "Macro";break; + case "0203": $tag = "Unknown";break; + case "0204": $tag = "DigiZoom";break; + case "0f00": $tag = "DataDump";break; + default: $tag = "unknown:".$tag;break; + } + + return $tag; +} + +//================= +// Formats Data for the data type +//==================================================================== +function formatSanyoData($type,$tag,$intel,$data) { + + if($type=="ASCII") { + + + } else if($type=="URATIONAL" || $type=="SRATIONAL") { + $data = unRational($data,$type,$intel); + + } else if($type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = rational($data,$type,$intel); + + if($tag=="0200") { //SpecialMode + if($data == 0) $data = (string) t("Normal"); + else $data = (string) t("Unknown").": ".$data; + } + if($tag=="0201") { //Quality + if($data == 2) $data = (string) t("High"); + else $data = (string) t("Unknown").": ".$data; + } + if($tag=="0202") { //Macro + if($data == 0) $data = (string) t("Normal"); + else $data = (string) t("Unknown").": ".$data; + } + } else if($type=="UNDEFINED") { + + + + } else { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + + return $data; +} + + + +//================= +// Sanyo Special data section +//==================================================================== +function parseSanyo($block,&$result,$seek, $globalOffset) { + + if($result['Endien']=="Intel") $intel=1; + else $intel=0; + + $model = $result['IFD0']['Model']; + + $place=8; //current place + $offset=8; + + //Get number of tags (2 bytes) + $num = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $num = intel2Moto($num); + $result['SubIFD']['MakerNote']['MakerNoteNumTags'] = hexdec($num); + + //loop thru all tags Each field is 12 bytes + for($i=0;$i<hexdec($num);$i++) { + + //2 byte tag + $tag = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $tag = intel2Moto($tag); + $tag_name = lookup_Sanyo_tag($tag); + + //2 byte type + $type = bin2hex(substr($block,$place,2));$place+=2; + if($intel==1) $type = intel2Moto($type); + lookup_type($type,$size); + + //4 byte count of number of data units + $count = bin2hex(substr($block,$place,4));$place+=4; + if($intel==1) $count = intel2Moto($count); + $bytesofdata = $size*hexdec($count); + + //4 byte value of data or pointer to data + $value = substr($block,$place,4);$place+=4; + + + if($bytesofdata<=4) { + $data = $value; + } else { + $value = bin2hex($value); + if($intel==1) $value = intel2Moto($value); + $v = fseek($seek,$globalOffset+hexdec($value)); //offsets are from TIFF header which is 12 bytes from the start of the file + if($v==0) { + $data = fread($seek, $bytesofdata); + } else if($v==-1) { + $result['Errors'] = $result['Errors']++; + } + } + $formated_data = formatSanyoData($type,$tag,$intel,$data); + + if($result['VerboseOutput']==1) { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + if($type=="URATIONAL" || $type=="SRATIONAL" || $type=="USHORT" || $type=="SSHORT" || $type=="ULONG" || $type=="SLONG" || $type=="FLOAT" || $type=="DOUBLE") { + $data = bin2hex($data); + if($intel==1) $data = intel2Moto($data); + } + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['RawData'] = $data; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Type'] = $type; + $result['SubIFD']['MakerNote'][$tag_name."_Verbose"]['Bytes'] = $bytesofdata; + } else { + $result['SubIFD']['MakerNote'][$tag_name] = $formated_data; + } + } +} + + +?>
\ No newline at end of file diff --git a/modules/exif/models/exif_key.php b/modules/exif/models/exif_key.php new file mode 100644 index 0000000..5c45669 --- /dev/null +++ b/modules/exif/models/exif_key.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Exif_Key_Model_Core extends ORM { +} diff --git a/modules/exif/models/exif_record.php b/modules/exif/models/exif_record.php new file mode 100644 index 0000000..1628ae4 --- /dev/null +++ b/modules/exif/models/exif_record.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Exif_Record_Model_Core extends ORM { +} diff --git a/modules/exif/module.info b/modules/exif/module.info new file mode 100644 index 0000000..9bbda95 --- /dev/null +++ b/modules/exif/module.info @@ -0,0 +1,7 @@ +name = "Exif Data" +description = "Extract Exif data and display it on photo pages." +version = 1 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:exif" +discuss_url = "http://galleryproject.org/forum_module_exif" diff --git a/modules/exif/views/exif_dialog.html.php b/modules/exif/views/exif_dialog.html.php new file mode 100644 index 0000000..22744e2 --- /dev/null +++ b/modules/exif/views/exif_dialog.html.php @@ -0,0 +1,33 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<style type="text/css"> + #g-exif-data { font-size: .85em; } + .g-odd { background: #bdd2ff; } + .g-even { background: #dfeffc; } +</style> +<h1 style="display: none;"><?= t("Photo detail") ?></h1> +<div id="g-exif-data"> + <table class="g-metadata" > + <tbody> + <? for ($i = 0; $i < count($details); $i++): ?> + <tr> + <td class="g-even"> + <?= $details[$i]["caption"] ?> + </td> + <td class="g-odd"> + <?= html::clean($details[$i]["value"]) ?> + </td> + <? if (!empty($details[++$i])): ?> + <td class="g-even"> + <?= $details[$i]["caption"] ?> + </td> + <td class="g-odd"> + <?= html::clean($details[$i]["value"]) ?> + </td> + <? else: ?> + <td class="g-even"></td><td class="g-odd"></td> + <? endif ?> + </tr> + <? endfor ?> + </tbody> + </table> +</div> diff --git a/modules/exif/views/exif_sidebar.html.php b/modules/exif/views/exif_sidebar.html.php new file mode 100644 index 0000000..8af2eb1 --- /dev/null +++ b/modules/exif/views/exif_sidebar.html.php @@ -0,0 +1,6 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<a id="g-exif-data-link" href="<?= url::site("exif/show/{$item->id}") ?>" title="<?= t("Photo details")->for_html_attr() ?>" + class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all"> + <span class="ui-icon ui-icon-info"></span> + <?= t("View more information") ?> +</a> diff --git a/modules/familygallery/controllers/login.php.backup b/modules/familygallery/controllers/login.php.backup new file mode 100644 index 0000000..4c5d558 --- /dev/null +++ b/modules/familygallery/controllers/login.php.backup @@ -0,0 +1,10 @@ +<?php +class zurLogin_Controller extends Login_Controller { + public function html() { + $view = new Theme_View("page.html", "other", "login"); + $view->page_title = t("Familien Fotogalerie"); + $view->content = new View("login_ajax.html"); + $view->content->form = auth::get_login_form("login/auth_html"); + print $view; + } +} diff --git a/modules/familygallery/helpers/familygallery_event.php b/modules/familygallery/helpers/familygallery_event.php new file mode 100644 index 0000000..6192455 --- /dev/null +++ b/modules/familygallery/helpers/familygallery_event.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access."); + +class familygallery_event { + /** + * remove the default login link and use our own + */ + static function user_menu($menu, $theme) { + $user = identity::active_user(); + if ($user->guest) { + // disable the default login +// $menu->remove('user_menu_login'); + // add ours +/* $menu->append(Menu::factory("dialog") + ->id("user_menu_fam") + ->css_id("g-fam-menu") + ->url(url::site("familygallery/html")) + ->label(t("Login")));*/ + } + } +} diff --git a/modules/familygallery/module.info b/modules/familygallery/module.info new file mode 100644 index 0000000..0f4906d --- /dev/null +++ b/modules/familygallery/module.info @@ -0,0 +1,7 @@ +name = "Family Gallery Module" +description = "This module contains all our customizations" +version = 1 +author_name = "" +author_url = "" +info_url = "" +discuss_url = "" diff --git a/modules/forge/libraries/Forge.php b/modules/forge/libraries/Forge.php new file mode 100644 index 0000000..9179aae --- /dev/null +++ b/modules/forge/libraries/Forge.php @@ -0,0 +1,323 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE (FORm GEneration) library. + * + * $Id: Forge.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Forge_Core { + + // Template variables + protected $template = array + ( + 'title' => '', + 'class' => '', + 'open' => '', + 'close' => '', + ); + + // Form attributes + protected $attr = array(); + + // Form inputs and hidden inputs + public $inputs = array(); + public $hidden = array(); + + // Error message format, only used with custom templates + public $error_format = '<p class="error">{message}</p>'; + public $newline_char = "\n"; + + /** + * Form constructor. Sets the form action, title, method, and attributes. + * + * @return void + */ + public function __construct($action = NULL, $title = '', $method = NULL, $attr = array()) + { + // Set form attributes + $this->attr['action'] = $action; + $this->attr['method'] = empty($method) ? 'post' : $method; + + // Set template variables + $this->template['title'] = $title; + + // Empty attributes sets the class to "form" + empty($attr) and $attr = array('class' => 'form'); + + // String attributes is the class name + is_string($attr) and $attr = array('class' => $attr); + + // Extend the template with the attributes + $this->attr += $attr; + } + + /** + * Magic __get method. Returns the specified form element. + * + * @param string unique input name + * @return object + */ + public function __get($key) + { + if (isset($this->inputs[$key])) + { + return $this->inputs[$key]; + } + elseif (isset($this->hidden[$key])) + { + return $this->hidden[$key]; + } + } + + /** + * Magic __call method. Creates a new form element object. + * + * @throws Kohana_Exception + * @param string input type + * @param string input name + * @return object + */ + public function __call($method, $args) + { + // Class name + $input = 'Form_'.ucfirst($method); + + // Create the input + switch (count($args)) + { + case 1: + $input = new $input($args[0]); + break; + case 2: + $input = new $input($args[0], $args[1]); + break; + default: + throw new Kohana_Exception('forge.invalid_input', $input); + } + + if ( ! ($input instanceof Form_Input) AND ! ($input instanceof Forge)) + throw new Kohana_Exception('forge.unknown_input', get_class($input)); + + $input->method = $this->attr['method']; + + if ($name = $input->name) + { + // Assign by name + if ($method == 'hidden') + { + $this->hidden[$name] = $input; + } + else + { + $this->inputs[$name] = $input; + } + } + else + { + // No name, these are unretrievable + $this->inputs[] = $input; + } + + return $input; + } + + /** + * Set a form attribute. This method is chainable. + * + * @param string|array attribute name, or an array of attributes + * @param string attribute value + * @return object + */ + public function set_attr($key, $val = NULL) + { + if (is_array($key)) + { + // Merge the new attributes with the old ones + $this->attr = array_merge($this->attr, $key); + } + else + { + // Set the new attribute + $this->attr[$key] = $val; + } + + return $this; + } + + /** + * Validates the form by running each inputs validation rules. + * + * @return bool + */ + public function validate() + { + $status = TRUE; + + $inputs = array_merge($this->hidden, $this->inputs); + + foreach ($inputs as $input) + { + if ($input->validate() == FALSE) + { + $status = FALSE; + } + } + + return $status; + } + + /** + * Returns the form as an array of input names and values. + * + * @return array + */ + public function as_array() + { + $data = array(); + foreach (array_merge($this->hidden, $this->inputs) as $input) + { + if (is_object($input->name)) // It's a Forge_Group object (hopefully) + { + foreach ($input->inputs as $group_input) + { + if ($name = $group_input->name) + { + $data[$name] = $group_input->value; + } + } + } + else if (is_array($input->inputs)) + { + foreach ($input->inputs as $group_input) + { + if ($name = $group_input->name) + { + $data[$name] = $group_input->value; + } + } + } + else if ($name = $input->name) // It's a normal input + { + // Return only named inputs + $data[$name] = $input->value; + } + } + return $data; + } + + /** + * Changes the error message format. Your message formatting must + * contain a {message} placeholder. + * + * @throws Kohana_Exception + * @param string new message format + * @return void + */ + public function error_format($string = '') + { + if (strpos((string) $string, '{message}') === FALSE) + throw new Kohana_Exception('validation.error_format'); + + $this->error_format = $string; + } + + /** + * Creates the form HTML + * + * @param string form view template name + * @param boolean use a custom view + * @return string + */ + public function render($template = 'forge_template', $custom = FALSE) + { + // Load template + $form = new View($template); + + if ($custom) + { + // Using a custom view + + $data = array(); + foreach (array_merge($this->hidden, $this->inputs) as $input) + { + $data[$input->name] = $input; + + // Groups will never have errors, so skip them + if ($input instanceof Form_Group) + continue; + + // Compile the error messages for this input + $messages = ''; + $errors = $input->error_messages(); + if (is_array($errors) AND ! empty($errors)) + { + foreach ($errors as $error) + { + // Replace the message with the error in the html error string + $messages .= str_replace('{message}', $error, $this->error_format).$this->newline_char; + } + } + + $data[$input->name.'_errors'] = $messages; + } + + $form->set($data); + } + else + { + // Using a template view + + $form->set($this->template); + $hidden = array(); + if ( ! empty($this->hidden)) + { + foreach ($this->hidden as $input) + { + $hidden['name'] = $input->name; + $hidden['value'] = $input->value; + } + } + + $form_type = 'open'; + // See if we need a multipart form + $check_inputs = array($this->inputs); + while ($check_inputs) { + foreach (array_shift($check_inputs) as $input) { + if ($input instanceof Form_Upload) + { + $form_type = 'open_multipart'; + } + if ($input instanceof Form_Group) + { + $check_inputs += array($input->inputs); + } + } + } + + // Set the form open and close + $form->open = form::$form_type(arr::remove('action', $this->attr), $this->attr); + foreach ($this->hidden as $hidden) { + $form->open .= form::hidden($hidden->name, $hidden->value); + } + $form->close = "</form>"; + + // Set the inputs + $form->inputs = $this->inputs; + } + + return $form; + } + + /** + * Returns the form HTML + */ + public function __toString() + { + return (string) $this->render(); + } + +} // End Forge diff --git a/modules/forge/libraries/Form_Checkbox.php b/modules/forge/libraries/Form_Checkbox.php new file mode 100644 index 0000000..aded4fd --- /dev/null +++ b/modules/forge/libraries/Form_Checkbox.php @@ -0,0 +1,83 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE checkbox input library. + * + * $Id: Form_Checkbox.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Checkbox_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'checkbox', + 'class' => 'checkbox', + 'value' => '1', + 'checked' => FALSE, + ); + + protected $protect = array('type'); + + public function __get($key) + { + if ($key == 'value') + { + // Return the value if the checkbox is checked + return $this->data['checked'] ? $this->data['value'] : NULL; + } + + return parent::__get($key); + } + + public function label($val = NULL) + { + if ($val === NULL) + { + // Do not display labels for checkboxes, labels wrap checkboxes + return ''; + } + else + { + $this->data['label'] = ($val === TRUE) ? utf8::ucwords(inflector::humanize($this->name)) : $val; + return $this; + } + } + + protected function html_element() + { + // Import the data + $data = $this->data; + + if (empty($data['checked'])) + { + // Not checked + unset($data['checked']); + } + else + { + // Is checked + $data['checked'] = 'checked'; + } + + if ($label = arr::remove('label', $data)) + { + // There must be one space before the text + $label = ' '.ltrim($label); + } + + return '<label>'.form::input($data).html::clean($label).'</label>'; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + // Makes the box checked if the value from POST is the same as the current value + $this->data['checked'] = ($this->input_value($this->name) == $this->data['value']); + } + +} // End Form Checkbox
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Checklist.php b/modules/forge/libraries/Form_Checklist.php new file mode 100644 index 0000000..4536d39 --- /dev/null +++ b/modules/forge/libraries/Form_Checklist.php @@ -0,0 +1,92 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE checklist input library. + * + * $Id: Form_Checklist.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Checklist_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'type' => 'checkbox', + 'class' => 'checklist', + 'options' => array(), + ); + + protected $protect = array('name', 'type'); + + public function __construct($name) + { + $this->data['name'] = $name; + } + + public function __get($key) + { + if ($key == 'value') + { + // Return the currently checked values + $array = array(); + foreach ($this->data['options'] as $id => $opt) + { + // Return the options that are checked + ($opt[1] === TRUE) and $array[] = $id; + } + return $array; + } + + return parent::__get($key); + } + + public function render() + { + // Import base data + $base_data = $this->data; + + // Make it an array + $base_data['name'] .= '[]'; + + // Newline + $nl = "\n"; + + $checklist = '<ul class="'.arr::remove('class', $base_data).'">'.$nl; + foreach (arr::remove('options', $base_data) as $val => $opt) + { + // New set of input data + $data = $base_data; + + // Get the title and checked status + list ($title, $checked) = $opt; + + // Set the name, value, and checked status + $data['value'] = $val; + $data['checked'] = $checked; + + $checklist .= '<li><label>'.form::checkbox($data).' '.html::purify($title).'</label></li>'.$nl; + } + $checklist .= '</ul>'; + + return $checklist; + } + + protected function load_value() + { + foreach ($this->data['options'] as $val => $checked) + { + if ($input = $this->input_value($this->data['name'])) + { + $this->data['options'][$val][1] = in_array($val, $input); + } + else + { + $this->data['options'][$val][1] = FALSE; + } + } + } + +} // End Form Checklist
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Dateselect.php b/modules/forge/libraries/Form_Dateselect.php new file mode 100644 index 0000000..a6e752e --- /dev/null +++ b/modules/forge/libraries/Form_Dateselect.php @@ -0,0 +1,138 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE dateselect input library. + * + * $Id: Form_Dateselect.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Dateselect_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'dropdown', + ); + + protected $protect = array('type'); + + // Precision for the parts, you can use @ to insert a literal @ symbol + protected $parts = array + ( + 'month' => array(), + 'day' => array(1), + 'year' => array(), + ' @ ', + 'hour' => array(), + ':', + 'minute' => array(5), + 'am_pm' => array(), + ); + + public function __construct($name) + { + // Set name + $this->data['name'] = $name; + + // Default to the current time + $this->data['value'] = time(); + } + + public function __call($method, $args) + { + if (isset($this->parts[substr($method, 0, -1)])) + { + // Set options for date generation + $this->parts[substr($method, 0, -1)] = $args; + return $this; + } + + return parent::__call($method, $args); + } + + public function html_element() + { + // Import base data + $data = $this->data; + + // Get the options and default selection + $time = $this->time_array(arr::remove('value', $data)); + + // No labels or values + unset($data['label']); + + $input = ''; + foreach ($this->parts as $type => $val) + { + if (is_int($type)) + { + // Just add the separators + $input .= $val; + continue; + } + + // Set this input name + $data['name'] = $this->data['name'].'['.$type.']'; + + // Set the selected option + $selected = $time[$type]; + + if ($type == 'am_pm') + { + // Options are static + $options = array('AM' => 'AM', 'PM' => 'PM'); + } + else + { + // minute(s), hour(s), etc + $type .= 's'; + + // Use the date helper to generate the options + $options = empty($val) ? date::$type() : call_user_func_array(array('date', $type), $val); + } + + $input .= form::dropdown($data, $options, $selected); + } + + return $input; + } + + protected function time_array($timestamp) + { + $time = array_combine + ( + array('month', 'day', 'year', 'hour', 'minute', 'am_pm'), + explode('--', date('n--j--Y--g--i--A', $timestamp)) + ); + + // Minutes should always be in 5 minute increments + $time['minute'] = num::round($time['minute'], current($this->parts['minute'])); + + return $time; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $time = $this->input_value($this->name); + + // Make sure all the required inputs keys are set + $time += $this->time_array(time()); + + $this->data['value'] = mktime + ( + date::adjust($time['hour'], $time['am_pm']), + $time['minute'], + 0, + $time['month'], + $time['day'], + $time['year'] + ); + } + +} // End Form Dateselect
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Dropdown.php b/modules/forge/libraries/Form_Dropdown.php new file mode 100644 index 0000000..9aa87a6 --- /dev/null +++ b/modules/forge/libraries/Form_Dropdown.php @@ -0,0 +1,78 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE dropdown input library. + * + * $Id: Form_Dropdown.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Dropdown_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'dropdown', + ); + + protected $protect = array('type'); + + public function __get($key) + { + if ($key == 'value') + { + return $this->selected; + } + + return parent::__get($key); + } + + public function html_element() + { + // Import base data + $base_data = $this->data; + + unset($base_data['label']); + + // Get the options and default selection + $options = arr::remove('options', $base_data); + $selected = arr::remove('selected', $base_data); + + return form::dropdown($base_data, $options, $selected); + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $this->data['selected'] = $this->input_value($this->name); + } + + public function validate() + { + // Validation has already run + if (is_bool($this->is_valid)) + return $this->is_valid; + + if ($this->input_value() == FALSE) + { + // No data to validate + return $this->is_valid = FALSE; + } + + // Load the submitted value + $this->load_value(); + + if ( ! array_key_exists($this->value, $this->data['options'])) + { + // Value does not exist in the options + return $this->is_valid = FALSE; + } + + return parent::validate(); + } + +} // End Form Dropdown
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Group.php b/modules/forge/libraries/Form_Group.php new file mode 100644 index 0000000..0a04912 --- /dev/null +++ b/modules/forge/libraries/Form_Group.php @@ -0,0 +1,89 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE group library. + * + * $Id: Form_Group.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Group_Core extends Forge { + + protected $data = array + ( + 'type' => 'group', + 'name' => '', + 'class' => 'group', + 'label' => '', + 'message' => '' + ); + + // Input method + public $method; + + public function __construct($name = NULL, $class = 'group') + { + $this->data['name'] = $name; + $this->data['class'] = $class; + + // Set dummy data so we don't get errors + $this->attr['action'] = ''; + $this->attr['method'] = 'post'; + } + + public function __get($key) + { + if ($key == 'type' || $key == 'name' || $key == 'label') + { + return $this->data[$key]; + } + return parent::__get($key); + } + + public function __set($key, $val) + { + if ($key == 'method') + { + $this->attr['method'] = $val; + } + $this->$key = $val; + } + + public function label($val = NULL) + { + if ($val === NULL) + { + if ($label = $this->data['label']) + { + return html::purify($this->data['label']); + } + } + else + { + $this->data['label'] = ($val === TRUE) ? ucwords(inflector::humanize($this->data['name'])) : $val; + return $this; + } + } + + public function message($val = NULL) + { + if ($val === NULL) + { + return $this->data['message']; + } + else + { + $this->data['message'] = $val; + return $this; + } + } + + public function render($template = 'forge_template', $custom = FALSE) + { + // No Sir, we don't want any html today thank you + return; + } + +} // End Form Group
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Hidden.php b/modules/forge/libraries/Form_Hidden.php new file mode 100644 index 0000000..423f626 --- /dev/null +++ b/modules/forge/libraries/Form_Hidden.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE hidden input library. + * + * $Id: Form_Hidden.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Hidden_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'value' => '', + ); + + public function render() + { + return form::hidden($this->data['name'], $this->data['value']); + } + +} // End Form Hidden
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Input.php b/modules/forge/libraries/Form_Input.php new file mode 100644 index 0000000..0c57801 --- /dev/null +++ b/modules/forge/libraries/Form_Input.php @@ -0,0 +1,555 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE base input library. + * + * $Id: Form_Input.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Input_Core { + + // Input method + public $method; + + // Element data + protected $data = array + ( + 'type' => 'text', + 'class' => 'textbox', + 'value' => '' + ); + + // Protected data keys + protected $protect = array(); + + // Validation rules, matches, and callbacks + protected $rules = array(); + protected $matches = array(); + protected $callbacks = array(); + + // Validation check + protected $is_valid; + + // Errors + protected $errors = array(); + protected $error_messages = array(); + + /** + * Sets the input element name. + */ + public function __construct($name) + { + $this->data['name'] = $name; + } + + /** + * Sets form attributes, or return rules. + */ + public function __call($method, $args) + { + if ($method == 'rules') + { + if (empty($args)) + return $this->rules; + + // Set rules and action + $rules = $args[0]; + $action = substr($rules, 0, 1); + + if (in_array($action, array('-', '+', '='))) + { + // Remove the action from the rules + $rules = substr($rules, 1); + } + else + { + // Default action is append + $action = ''; + } + + $this->add_rules(explode('|', $rules), $action); + } + elseif ($method == 'name') + { + // Do nothing. The name should stay static once it is set. + } + else + { + $this->data[$method] = $args[0]; + } + + return $this; + } + + /** + * Returns form attributes. + * + * @param string attribute name + * @return string + */ + public function __get($key) + { + if (isset($this->data[$key])) + { + return $this->data[$key]; + } + } + + /** + * Sets a form element that this element must match the value of. + * + * @chainable + * @param object another Forge input + * @return object + */ + public function matches($input) + { + if ( ! in_array($input, $this->matches, TRUE)) + { + $this->matches[] = $input; + } + + return $this; + } + + /** + * Sets a callback method as a rule for this input. + * + * @chainable + * @param callback + * @return object + */ + public function callback($callback) + { + if ( ! in_array($callback, $this->callbacks, TRUE)) + { + $this->callbacks[] = $callback; + } + + return $this; + } + + /** + * Sets or returns the input label. + * + * @chainable + * @param string label to set + * @return string|object + */ + public function label($val = NULL) + { + if ($val === NULL) + { + if (isset($this->data['name']) AND isset($this->data['label'])) + { + return form::label($this->data['name'], $this->data['label']); + } + return FALSE; + } + else + { + $this->data['label'] = ($val === TRUE) ? utf8::ucwords(inflector::humanize($this->name)) : $val; + return $this; + } + } + + /** + * Set or return the error message. + * + * @chainable + * @param string error message + * @return strong|object + */ + public function message($val = NULL) + { + if ($val === NULL) + { + if (isset($this->data['message'])) + return $this->data['message']; + } + else + { + $this->data['message'] = $val; + return $this; + } + } + + /** + * Runs validation and returns the element HTML. + * + * @return string + */ + public function render() + { + // Make sure validation runs + $this->validate(); + + return $this->html_element(); + } + + /** + * Returns the form input HTML. + * + * @return string + */ + protected function html_element() + { + $data = $this->data; + + unset($data['label']); + unset($data['message']); + + return form::input($data); + } + + /** + * Replace, remove, or append rules. + * + * @param array rules to change + * @param string action to use: replace, remove, append + */ + protected function add_rules( array $rules, $action) + { + if ($action === '=') + { + // Just replace the rules + $this->rules = $rules; + return; + } + + foreach ($rules as $rule) + { + if ($action === '-') + { + if (($key = array_search($rule, $this->rules)) !== FALSE) + { + // Remove the rule + unset($this->rules[$key]); + } + } + else + { + if ( ! in_array($rule, $this->rules)) + { + if ($action == '+') + { + array_unshift($this->rules, $rule); + } + else + { + $this->rules[] = $rule; + } + } + } + } + } + + /** + * Add an error to the input. + * + * @chainable + * @return object + */ + public function add_error($key, $val) + { + if ( ! isset($this->errors[$key])) + { + $this->errors[$key] = $val; + } + + return $this; + } + + /** + * Set or return the error messages. + * + * @chainable + * @param string|array failed validation function, or an array of messages + * @param string error message + * @return object|array + */ + public function error_messages($func = NULL, $message = NULL) + { + // Set custom error messages + if ( ! empty($func)) + { + if (is_array($func)) + { + // Replace all + $this->error_messages = $func; + } + else + { + if (empty($message)) + { + // Single error, replaces all others + $this->error_messages = $func; + } + else + { + // Add custom error + $this->error_messages[$func] = $message; + } + } + return $this; + } + + // Make sure validation runs + is_null($this->is_valid) and $this->validate(); + + // Return single error + if ( ! is_array($this->error_messages) AND ! empty($this->errors)) + return array($this->error_messages); + + $messages = array(); + foreach ($this->errors as $func => $args) + { + if (is_string($args)) + { + $error = $args; + } + else + { + // Force args to be an array + $args = is_array($args) ? $args : array(); + + // Add the label or name to the beginning of the args + array_unshift($args, $this->label ? mb_strtolower($this->label) : $this->name); + + if (isset($this->error_messages[$func])) + { + // Use custom error message + $error = vsprintf($this->error_messages[$func], $args); + } + else + { + // Get the proper i18n entry, very hacky but it works + switch ($func) + { + case 'valid_url': + case 'valid_email': + case 'valid_ip': + // Fetch an i18n error message + $error = 'validation.'.$func; + break; + case substr($func, 0, 6) === 'valid_': + // Strip 'valid_' from func name + $func = (substr($func, 0, 6) === 'valid_') ? substr($func, 6) : $func; + case 'alpha': + case 'alpha_dash': + case 'digit': + case 'numeric': + // i18n strings have to be inserted into valid_type + $args[] = 'validation.'.$func; + $error = 'validation.valid_type'; + break; + default: + $error = 'validation.'.$func; + } + } + } + + // Add error to list + $messages[] = $error; + } + + return $messages; + } + + /** + * Get the global input value. + * + * @return string|bool + */ + protected function input_value($name = array()) + { + // Get the Input instance + $input = Input::instance(); + + // Fetch the method for this object + $method = $this->method; + + return $input->$method($name, NULL); + } + + /** + * Load the value of the input, if form data is present. + * + * @return void + */ + protected function load_value() + { + if (is_bool($this->is_valid)) + return; + + if ($name = $this->name) + { + // Load POSTed value, but only for named inputs + $this->data['value'] = $this->input_value($name); + } + + if (is_string($this->data['value'])) + { + // Trim string values + $this->data['value'] = trim($this->data['value']); + } + } + + /** + * Validate this input based on the set rules. + * + * @return bool + */ + public function validate() + { + // Validation has already run + if (is_bool($this->is_valid)) + return $this->is_valid; + + // No data to validate + if ($this->input_value() == FALSE) + return $this->is_valid = FALSE; + + // Load the submitted value + $this->load_value(); + + // No rules to validate + if (count($this->rules) == 0 AND count($this->matches) == 0 AND count($this->callbacks) == 0) + return $this->is_valid = TRUE; + + if ( ! empty($this->rules)) + { + foreach ($this->rules as $rule) + { + if (($offset = strpos($rule, '[')) !== FALSE) + { + // Get the args + $args = preg_split('/, ?/', trim(substr($rule, $offset), '[]')); + + // Remove the args from the rule + $rule = substr($rule, 0, $offset); + } + + if (substr($rule, 0, 6) === 'valid_' AND method_exists('valid', substr($rule, 6))) + { + $func = substr($rule, 6); + + if ($this->value AND ! valid::$func($this->value)) + { + $this->errors[$rule] = TRUE; + } + } + elseif (method_exists($this, 'rule_'.$rule)) + { + // The rule function is always prefixed with rule_ + $rule = 'rule_'.$rule; + + if (isset($args)) + { + // Manually call up to 2 args for speed + switch (count($args)) + { + case 1: + $this->$rule($args[0]); + break; + case 2: + $this->$rule($args[0], $args[1]); + break; + default: + call_user_func_array(array($this, $rule), $args); + break; + } + } + else + { + // Just call the rule + $this->$rule(); + } + + // Prevent args from being re-used + unset($args); + } + else + { + throw new Kohana_Exception('validation.invalid_rule', $rule); + } + + // Stop when an error occurs + if ( ! empty($this->errors)) + break; + } + } + + if ( ! empty($this->matches)) + { + foreach ($this->matches as $input) + { + if ($this->value != $input->value) + { + // Field does not match + $this->errors['matches'] = array($input->label ? mb_strtolower($input->label) : $input->name); + break; + } + } + } + + if ( ! empty($this->callbacks)) + { + foreach ($this->callbacks as $callback) + { + call_user_func($callback, $this); + + // Stop when an error occurs + if ( ! empty($this->errors)) + break; + } + } + + // If there are errors, validation failed + return $this->is_valid = empty($this->errors); + } + + /** + * Validate required. + */ + protected function rule_required() + { + if ($this->value === '' OR $this->value === NULL) + { + $this->errors['required'] = TRUE; + } + } + + /** + * Validate length. + */ + protected function rule_length($min, $max = NULL) + { + // Get the length, return if zero + if (($length = mb_strlen($this->value)) === 0) + return; + + if ($max == NULL) + { + if ($length != $min) + { + $this->errors['exact_length'] = array($min); + } + } + else + { + if ($length < $min) + { + $this->errors['min_length'] = array($min); + } + elseif ($length > $max) + { + $this->errors['max_length'] = array($max); + } + } + } + +} // End Form Input
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Password.php b/modules/forge/libraries/Form_Password.php new file mode 100644 index 0000000..2622ae5 --- /dev/null +++ b/modules/forge/libraries/Form_Password.php @@ -0,0 +1,23 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE password input library. + * + * $Id: Form_Password.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Password_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'password', + 'class' => 'password', + 'value' => '', + ); + + protected $protect = array('type'); + +} // End Form Password
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Phonenumber.php b/modules/forge/libraries/Form_Phonenumber.php new file mode 100644 index 0000000..2befef5 --- /dev/null +++ b/modules/forge/libraries/Form_Phonenumber.php @@ -0,0 +1,98 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE phone number input library. + * + * $Id: Form_Phonenumber.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Phonenumber_Core extends Form_Input { + + protected $data = array + ( + 'name' => '', + 'class' => 'phone_number', + ); + + protected $protect = array('type'); + + // Precision for the parts, you can use @ to insert a literal @ symbol + protected $parts = array + ( + 'area_code' => '', + 'exchange' => '', + 'last_four' => '', + ); + + public function __construct($name) + { + // Set name + $this->data['name'] = $name; + } + + public function __call($method, $args) + { + if (isset($this->parts[substr($method, 0, -1)])) + { + // Set options for date generation + $this->parts[substr($method, 0, -1)] = $args; + return $this; + } + + return parent::__call($method, $args); + } + + public function html_element() + { + // Import base data + $data = $this->data; + + $input = ''; + foreach ($this->parts as $type => $val) + { + isset($data['value']) OR $data['value'] = ''; + $temp = $data; + $temp['name'] = $this->data['name'].'['.$type.']'; + $offset = (strlen($data['value']) == 10) ? 0 : 3; + switch ($type) + { + case 'area_code': + if (strlen($data['value']) == 10) + { + $temp['value'] = substr($data['value'], 0, 3); + } + else + $temp['value'] = ''; + $temp['class'] = 'area_code'; + $input .= form::input(array_merge(array('value' => $val), $temp)).'-'; + break; + case 'exchange': + $temp['value'] = substr($data['value'], (3-$offset), 3); + $temp['class'] = 'exchange'; + $input .= form::input(array_merge(array('value' => $val), $temp)).'-'; + break; + case 'last_four': + $temp['value'] = substr($data['value'], (6-$offset), 4); + $temp['class'] = 'last_four'; + $input .= form::input(array_merge(array('value' => $val), $temp)); + break; + } + + } + + return $input; + } + + protected function load_value() + { + if (is_bool($this->valid)) + return; + + $data = $this->input_value($this->name, $this->data['name']); + + $this->data['value'] = $data['area_code'].$data['exchange'].$data['last_four']; + } +} // End Form Phonenumber
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Radio.php b/modules/forge/libraries/Form_Radio.php new file mode 100644 index 0000000..6648fd2 --- /dev/null +++ b/modules/forge/libraries/Form_Radio.php @@ -0,0 +1,22 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE radio input library. + * + * $Id: Form_Radio.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Radio_Core extends Form_Checkbox { + + protected $data = array + ( + 'type' => 'radio', + 'class' => 'radio', + 'value' => '1', + 'checked' => FALSE, + ); + +} // End Form_Radio
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Submit.php b/modules/forge/libraries/Form_Submit.php new file mode 100644 index 0000000..6908f6f --- /dev/null +++ b/modules/forge/libraries/Form_Submit.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE submit input library. + * + * $Id: Form_Submit.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Submit_Core extends Form_Input { + + protected $data = array + ( + 'type' => 'submit', + 'class' => 'submit' + ); + + protected $protect = array('type'); + + public function render() + { + $data = $this->data; + unset($data['label']); + + return form::submit($data); + } + +} // End Form Submit
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Textarea.php b/modules/forge/libraries/Form_Textarea.php new file mode 100644 index 0000000..279ea56 --- /dev/null +++ b/modules/forge/libraries/Form_Textarea.php @@ -0,0 +1,31 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE textarea input library. + * + * $Id: Form_Textarea.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Textarea_Core extends Form_Input { + + protected $data = array + ( + 'class' => 'textarea', + 'value' => '', + ); + + protected $protect = array('type'); + + protected function html_element() + { + $data = $this->data; + + unset($data['label']); + + return form::textarea($data); + } + +} // End Form Textarea
\ No newline at end of file diff --git a/modules/forge/libraries/Form_Upload.php b/modules/forge/libraries/Form_Upload.php new file mode 100644 index 0000000..eda9c8a --- /dev/null +++ b/modules/forge/libraries/Form_Upload.php @@ -0,0 +1,191 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * FORGE upload input library. + * + * $Id: Form_Upload.php 3326 2008-08-09 21:24:30Z Shadowhand $ + * + * @package Forge + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Form_Upload_Core extends Form_Input { + + protected $data = array + ( + 'class' => 'upload', + 'value' => '', + ); + + protected $protect = array('type', 'label', 'value'); + + // Upload data + protected $upload; + + // Upload directory and filename + protected $directory; + protected $filename; + + public function __construct($name, $filename = FALSE) + { + parent::__construct($name); + + if ( ! empty($_FILES[$name])) + { + if (empty($_FILES[$name]['tmp_name']) OR is_uploaded_file($_FILES[$name]['tmp_name'])) + { + // Cache the upload data in this object + $this->upload = $_FILES[$name]; + + // Hack to allow file-only inputs, where no POST data is present + $_POST[$name] = $this->upload['name']; + + // Set the filename + $this->filename = empty($filename) ? FALSE : $filename; + } + else + { + // Attempt to delete the invalid file + is_writable($_FILES[$name]['tmp_name']) and unlink($_FILES[$name]['tmp_name']); + + // Invalid file upload, possible hacking attempt + unset($_FILES[$name]); + } + } + } + + /** + * Sets the upload directory. + * + * @param string upload directory + * @return void + */ + public function directory($dir = NULL) + { + // Use the global upload directory by default + empty($dir) and $dir = Kohana::config('upload.directory'); + + // Make the path asbolute and normalize it + $directory = str_replace('\\', '/', realpath($dir)).'/'; + + // Make sure the upload director is valid and writable + if ($directory === '/' OR ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('upload.not_writable', $dir); + + $this->directory = $directory; + } + + public function validate() + { + // The upload directory must always be set + empty($this->directory) and $this->directory(); + + // By default, there is no uploaded file + $filename = ''; + + if ($status = parent::validate() AND $this->upload['error'] === UPLOAD_ERR_OK) + { + // Set the filename to the original name + $filename = $this->upload['name']; + + if (Kohana::config('upload.remove_spaces')) + { + // Remove spaces, due to global upload configuration + $filename = preg_replace('/\s+/', '_', $this->data['value']); + } + + if (file_exists($filepath = $this->directory.$filename)) + { + if ($this->filename !== TRUE OR ! is_writable($filepath)) + { + // Prefix the file so that the filename is unique + $filepath = $this->directory.'uploadfile-'.uniqid(time()).'-'.$this->upload['name']; + } + } + + // Move the uploaded file to the upload directory + move_uploaded_file($this->upload['tmp_name'], $filepath); + } + + if ( ! empty($_POST[$this->data['name']])) + { + // Reset the POST value to the new filename + $this->data['value'] = $_POST[$this->data['name']] = empty($filepath) ? '' : $filepath; + } + + return $status; + } + + protected function rule_required() + { + if (empty($this->upload) OR $this->upload['error'] === UPLOAD_ERR_NO_FILE) + { + $this->errors['required'] = TRUE; + } + } + + public function rule_allow() + { + if (empty($this->upload['tmp_name']) OR count($types = func_get_args()) == 0) + return; + + if (($mime = file::mime($this->upload['tmp_name'])) === FALSE) + { + // Trust the browser + $mime = $this->upload['type']; + } + + // Get rid of the ";charset=binary" that can occasionally occur and is + // legal via RFC2045 + $mime = preg_replace('/; charset=binary/', '', $mime); + + // Allow nothing by default + $allow = FALSE; + + foreach ($types as $type) + { + // Load the mime types + $type = Kohana::config('mimes.'.$type); + + if (is_array($type) AND in_array($mime, $type)) + { + // Type is valid + $allow = TRUE; + break; + } + } + + if ($allow === FALSE) + { + $this->errors['invalid_type'] = TRUE; + } + } + + public function rule_size($size) + { + // Skip the field if it is empty + if (empty($this->upload) OR $this->upload['error'] === UPLOAD_ERR_NO_FILE) + return; + + $bytes = (int) $size; + + switch (substr($size, -2)) + { + case 'GB': $bytes *= 1024; + case 'MB': $bytes *= 1024; + case 'KB': $bytes *= 1024; + default: break; + } + + if (empty($this->upload['size']) OR $this->upload['size'] > $bytes) + { + $this->errors['max_size'] = array($size); + } + } + + protected function html_element() + { + return form::upload($this->data); + } + +} // End Form Upload diff --git a/modules/g2_import/controllers/admin_g2_import.php b/modules/g2_import/controllers/admin_g2_import.php new file mode 100644 index 0000000..c4f0390 --- /dev/null +++ b/modules/g2_import/controllers/admin_g2_import.php @@ -0,0 +1,136 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_g2_import_Controller extends Admin_Controller { + public function index() { + g2_import::lower_error_reporting(); + if (g2_import::is_configured()) { + g2_import::init(); + } + + $view = new Admin_View("admin.html"); + $view->page_title = t("Gallery 2 import"); + $view->content = new View("admin_g2_import.html"); + + if (class_exists("GalleryCoreApi")) { + $view->content->g2_stats = $g2_stats = g2_import::g2_stats(); + $view->content->g3_stats = $g3_stats = g2_import::g3_stats(); + $view->content->g2_sizes = g2_import::common_sizes(); + $view->content->g2_version = g2_import::version(); + + // Don't count tags because we don't track them in g2_map + $view->content->g2_resource_count = + $g2_stats["users"] + $g2_stats["groups"] + $g2_stats["albums"] + + $g2_stats["photos"] + $g2_stats["movies"] + $g2_stats["comments"]; + $view->content->g3_resource_count = + $g3_stats["user"] + $g3_stats["group"] + $g3_stats["album"] + + $g3_stats["item"] + $g3_stats["comment"] + $g3_stats["tag"]; + } + + $view->content->form = $this->_get_import_form(); + $view->content->version = ""; + $view->content->thumb_size = module::get_var("gallery", "thumb_size"); + $view->content->resize_size = module::get_var("gallery", "resize_size"); + + if (g2_import::is_initialized()) { + if ((bool)ini_get("eaccelerator.enable") || (bool)ini_get("xcache.cacher")) { + message::warning(t("The eAccelerator and XCache PHP performance extensions are known to cause issues. If you're using either of those and are having problems, please disable them while you do your import. Add the following lines: <pre>%lines</pre> to gallery3/.htaccess and remove them when the import is done.", array("lines" => "\n\n php_value eaccelerator.enable 0\n php_value xcache.cacher off\n php_value xcache.optimizer off\n\n"))); + } + + foreach (array("notification", "search", "exif") as $module_id) { + if (module::is_active($module_id)) { + message::warning( + t("<a href=\"%url\">Deactivating</a> the <b>%module_id</b> module during your import will make it faster", + array("url" => url::site("admin/modules"), "module_id" => $module_id))); + } + } + if (module::is_active("akismet")) { + message::warning( + t("The Akismet module may mark some or all of your imported comments as spam. <a href=\"%url\">Deactivate</a> it to avoid that outcome.", + array("url" => url::site("admin/modules")))); + } + } else if (g2_import::is_configured()) { + $view->content->form->configure_g2_import->embed_path->add_error("invalid", 1); + } + g2_import::restore_error_reporting(); + print $view; + } + + public function save() { + access::verify_csrf(); + g2_import::lower_error_reporting(); + + $form = $this->_get_import_form(); + if ($form->validate()) { + $embed_path = $form->configure_g2_import->embed_path->value; + if (!is_file($embed_path) && file_exists("$embed_path/embed.php")) { + $embed_path = "$embed_path/embed.php"; + } + + if (($g2_init_error = g2_import::is_valid_embed_path($embed_path)) == "ok") { + message::success(t("Gallery 2 path saved")); + module::set_var("g2_import", "embed_path", $embed_path); + url::redirect("admin/g2_import"); + } else { + $form->configure_g2_import->embed_path->add_error($g2_init_error, 1); + } + } + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_g2_import.html"); + $view->content->form = $form; + g2_import::restore_error_reporting(); + print $view; + } + + public function autocomplete() { + $directories = array(); + $path_prefix = Input::instance()->get("q"); + foreach (glob("{$path_prefix}*") as $file) { + if (is_dir($file) && !is_link($file)) { + $file = html::clean($file); + $directories[] = $file; + + // If we find an embed.php, include it as well + if (file_exists("$file/embed.php")) { + $directories[] = "$file/embed.php"; + } + } + } + + ajax::response(implode("\n", $directories)); + } + + private function _get_import_form() { + $embed_path = module::get_var("g2_import", "embed_path", ""); + $form = new Forge( + "admin/g2_import/save", "", "post", array("id" => "g-admin-configure-g2-import-form")); + $group = $form->group("configure_g2_import")->label(t("Configure Gallery 2 Import")); + $group->input("embed_path")->label(t("Filesystem path to your Gallery 2 embed.php file")) + ->value($embed_path); + $group->embed_path->error_messages( + "invalid", t("The path you entered is not a Gallery 2 installation.")); + $group->embed_path->error_messages( + "broken", t("Your Gallery 2 install isn't working properly. Please verify it!")); + $group->embed_path->error_messages( + "missing", t("The path you entered does not exist.")); + $group->submit("")->value($embed_path ? t("Change") : t("Continue")); + return $form; + } +}
\ No newline at end of file diff --git a/modules/g2_import/controllers/g2.php b/modules/g2_import/controllers/g2.php new file mode 100644 index 0000000..c24d52e --- /dev/null +++ b/modules/g2_import/controllers/g2.php @@ -0,0 +1,121 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class G2_Controller extends Controller { + /** + * Redirect Gallery 2 urls to their appropriate matching Gallery 3 url. + * + * We use mod_rewrite to create this path, so Gallery2 urls like this: + * /gallery2/v/Family/Wedding.jpg.html + * /gallery2/main.php?g2_view=core.ShowItem&g2_itemId=1234 + * + * Show up here like this: + * /g2/map?path=v/Family/Wedding.jpg.html + * /g2/map?g2_view=core.ShowItem&g2_itemId=1931 + */ + public function map() { + $input = Input::instance(); + $path = $input->get("path"); + $id = $input->get("g2_itemId"); + $view = $input->get("g2_view"); + + // Tags did not have mappings created, so we need to catch them first. However, if a g2_itemId was + // passed, we'll want to show lookup the mapping anyway + if (($path && 0 === strpos($path, "tag/")) || $view == "tags.VirtualAlbum") { + if (0 === strpos($path, "tag/")) { + $tag_name = substr($path, 4); + } + if ($view == "tags.VirtualAlbum") { + $tag_name = $input->get("g2_tagName"); + } + + if (!$id) { + url::redirect("tag_name/$tag_name", 301); + } + + $tag = ORM::factory("tag")->where("name", "=", $tag_name)->find(); + if ($tag->loaded()) { + item::set_display_context_callback("Tag_Controller::get_display_context", $tag->id); + // We want to show the item as part of the tag virtual album. Most of this code is below; we'll + // change $path and $view to let it fall through + $view = ""; + $path = ""; + } + } + + if (($path && $path != 'index.php' && $path != 'main.php') || $id) { + if ($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)); + if ($view == "core.DownloadItem") { + $where[] = array("resource_type", "IN", array("file", "resize", "thumbnail", "full")); + } else if ($view) { + $where[] = array("g2_url", "LIKE", "%" . Database::escape_for_like("g2_view=$view") . "%"); + } // else: Assuming that the first search hit is sufficiently good. + } else if ($path) { + $where = array(array("g2_url", "IN", array($path, str_replace(" ", "+", $path)))); + } else { + throw new Kohana_404_Exception(); + } + + $g2_map = ORM::factory("g2_map") + ->merge_where($where) + ->find(); + + if (!$g2_map->loaded()) { + throw new Kohana_404_Exception(); + } + + $item = ORM::factory("item", $g2_map->g3_id); + if (!$item->loaded()) { + throw new Kohana_404_Exception(); + } + $resource_type = $g2_map->resource_type; + } else { + $item = item::root(); + $resource_type = "album"; + } + access::required("view", $item); + + + // Redirect the user to the new url + switch ($resource_type) { + case "thumbnail": + url::redirect($item->thumb_url(true), 301); + + case "resize": + url::redirect($item->resize_url(true), 301); + + case "file": + case "full": + url::redirect($item->file_url(true), 301); + + case "item": + case "album": + url::redirect($item->abs_url(), 301); + + case "group": + case "user": + default: + throw new Kohana_404_Exception(); + } + } +} diff --git a/modules/g2_import/data/broken-image.gif b/modules/g2_import/data/broken-image.gif Binary files differnew file mode 100755 index 0000000..fb9c824 --- /dev/null +++ b/modules/g2_import/data/broken-image.gif diff --git a/modules/g2_import/helpers/g2_import.php b/modules/g2_import/helpers/g2_import.php new file mode 100644 index 0000000..82850e8 --- /dev/null +++ b/modules/g2_import/helpers/g2_import.php @@ -0,0 +1,1375 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class g2_import_Core { + public static $init = false; + public static $map = array(); + public static $g2_base_url = null; + + private static $current_g2_item = null; + private static $error_reporting = null; + + static function is_configured() { + return module::get_var("g2_import", "embed_path"); + } + + static function is_initialized() { + return g2_import::$init == "ok"; + } + + static function init() { + if (g2_import::$init) { + return; + } + + $embed_path = module::get_var("g2_import", "embed_path"); + if (empty($embed_path)) { + throw new Exception("@todo G2_IMPORT_NOT_CONFIGURED"); + } + + g2_import::$init = g2_import::init_embed($embed_path); + } + + static function is_valid_embed_path($embed_path) { + $mod_path = VARPATH . "modules/g2_import/" . md5($embed_path); + if (file_exists($mod_path)) { + dir::unlink($mod_path); + } + return g2_import::init_embed($embed_path); + } + + /** + * Initialize the embedded Gallery 2 instance. Call this before any other Gallery 2 calls. + * + * Return values: + * "ok" - the Gallery 2 install is fine + * "missing" - the embed path does not exist + * "invalid" - the install path is not a valid Gallery 2 code base + * "broken" - the embed path is correct, but the Gallery 2 install is broken + */ + static function init_embed($embed_path) { + if (!is_file($embed_path)) { + return "missing"; + } + + try { + // Gallery 2 defines a class called Gallery. So does Gallery 3. They don't get along. So do + // a total hack here and copy over a few critical files (embed.php, main.php, bootstrap.inc + // and Gallery.class) and munge them so that we can rename the Gallery class to be + // G2_Gallery. Is this retarded? Why yes it is. + // + // Store the munged files in a directory that's the md5 hash of the embed path so that + // multiple import sources don't interfere with each other. + + $mod_path = VARPATH . "modules/g2_import/" . md5($embed_path); + if (!file_exists($mod_path) || !file_exists("$mod_path/embed.php")) { + @dir::unlink($mod_path); + mkdir($mod_path); + + $config_dir = dirname($embed_path); + if (filesize($embed_path) > 200) { + // Regular install + $base_dir = $config_dir; + } else { + // Multisite install. Line 2 of embed.php will be something like: + // require('/usr/home/bharat/public_html/gallery2/embed.php'); + $lines = file($embed_path); + preg_match("#require\('(.*)/embed.php'\);#", $lines[2], $matches); + $base_dir = $matches[1]; + } + + file_put_contents( + "$mod_path/embed.php", + str_replace( + array( + "require_once(dirname(__FILE__) . '/modules/core/classes/GalleryDataCache.class');", + "require(dirname(__FILE__) . '/modules/core/classes/GalleryEmbed.class');"), + array( + "require_once('$base_dir/modules/core/classes/GalleryDataCache.class');", + "require('$base_dir/modules/core/classes/GalleryEmbed.class');"), + array_merge( + array("<?php defined(\"SYSPATH\") or die(\"No direct script access.\") ?>\n"), + file("$base_dir/embed.php")))); + + file_put_contents( + "$mod_path/main.php", + str_replace( + array( + "include(dirname(__FILE__) . '/bootstrap.inc');", + "require_once(dirname(__FILE__) . '/init.inc');"), + array( + "include(dirname(__FILE__) . '/bootstrap.inc');", + "require_once('$base_dir/init.inc');"), + array_merge( + array("<?php defined(\"SYSPATH\") or die(\"No direct script access.\") ?>\n"), + file("$base_dir/main.php")))); + + file_put_contents( + "$mod_path/bootstrap.inc", + str_replace( + array( + "require_once(dirname(__FILE__) . '/modules/core/classes/Gallery.class');", + "require_once(dirname(__FILE__) . '/modules/core/classes/GalleryDataCache.class');", + "define('GALLERY_CONFIG_DIR', dirname(__FILE__));", + "\$gallery =& new Gallery();", + "\$GLOBALS['gallery'] =& new Gallery();", + "\$gallery = new Gallery();"), + array( + "require_once(dirname(__FILE__) . '/Gallery.class');", + "require_once('$base_dir/modules/core/classes/GalleryDataCache.class');", + "define('GALLERY_CONFIG_DIR', '$config_dir');", + "\$gallery =& new G2_Gallery();", + "\$GLOBALS['gallery'] =& new G2_Gallery();", + "\$gallery = new G2_Gallery();"), + array_merge( + array("<?php defined(\"SYSPATH\") or die(\"No direct script access.\") ?>\n"), + file("$base_dir/bootstrap.inc")))); + + file_put_contents( + "$mod_path/Gallery.class", + str_replace( + array("class Gallery", + "function Gallery"), + array("class G2_Gallery", + "function G2_Gallery"), + array_merge( + array("<?php defined(\"SYSPATH\") or die(\"No direct script access.\") ?>\n"), + file("$base_dir/modules/core/classes/Gallery.class")))); + } else { + // Ok, this is a good one. If you're running a bytecode accelerator and you move your + // Gallery install, these files sometimes get cached with the wrong path and then fail to + // load properly. + // Documented in https://sourceforge.net/apps/trac/gallery/ticket/1253 + touch("$mod_path/embed.php"); + touch("$mod_path/main.php"); + touch("$mod_path/bootstrap.inc"); + touch("$mod_path/Gallery.class.inc"); + } + + require("$mod_path/embed.php"); + if (!class_exists("GalleryEmbed")) { + return "invalid"; + } + + $ret = GalleryEmbed::init(); + if ($ret) { + Kohana_Log::add("error", "Gallery 2 call failed with: " . $ret->getAsText()); + return "broken"; + } + + $admin_group_id = g2(GalleryCoreApi::getPluginParameter("module", "core", "id.adminGroup")); + $admins = g2(GalleryCoreApi::fetchUsersForGroup($admin_group_id, 1)); + $admin_id = current(array_flip($admins)); + $admin = g2(GalleryCoreApi::loadEntitiesById($admin_id)); + $GLOBALS["gallery"]->setActiveUser($admin); + + // Make sure we have an embed location so that embedded url generation comes out ok. Without + // this, the Gallery2 ModRewrite code won't try to do url generation. + $g2_embed_location = + g2(GalleryCoreApi::getPluginParameter("module", "rewrite", "modrewrite.embeddedLocation")); + + if (empty($g2_embed_location)) { + $g2_embed_location = + g2(GalleryCoreApi::getPluginParameter("module", "rewrite", "modrewrite.galleryLocation")); + g2(GalleryCoreApi::setPluginParameter("module", "rewrite", "modrewrite.embeddedLocation", + $g2_embed_location)); + g2($gallery->getStorage()->checkPoint()); + } + + if ($g2_embed_location) { + self::$g2_base_url = $g2_embed_location; + } else { + self::$g2_base_url = $GLOBALS["gallery"]->getUrlGenerator()->generateUrl( + array(), + array("forceSessionId" => false, + "htmlEntities" => false, + "urlEncode" => false, + "useAuthToken" => false)); + } + } catch (ErrorException $e) { + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + return "broken"; + } + + return "ok"; + } + + /** + * Return the version of Gallery 2 (eg "2.3") + */ + static function version() { + $core = g2(GalleryCoreApi::loadPlugin("module", "core")); + $versions = $core->getInstalledVersions(); + return $versions["gallery"]; + } + + /** + * Return true if the given Gallery 2 module is active. + */ + static function g2_module_active($module) { + static $plugin_list; + if (!$plugin_list) { + $plugin_list = g2(GalleryCoreApi::fetchPluginList("module")); + } + + return @$plugin_list[$module]["active"]; + } + + /** + * Return a set of statistics about the number of users, groups, albums, photos, movies and + * comments available for import from the Gallery 2 instance. + */ + static function g2_stats() { + global $gallery; + $root_album_id = g2(GalleryCoreApi::getDefaultAlbumId()); + $stats["users"] = g2(GalleryCoreApi::fetchUserCount()); + $stats["groups"] = g2(GalleryCoreApi::fetchGroupCount()); + $stats["albums"] = g2(GalleryCoreApi::fetchItemIdCount("GalleryAlbumItem")); + $stats["photos"] = g2(GalleryCoreApi::fetchItemIdCount("GalleryPhotoItem")); + $stats["movies"] = g2(GalleryCoreApi::fetchItemIdCount("GalleryMovieItem")); + + if (g2_import::g2_module_active("comment") && module::is_active("comment")) { + GalleryCoreApi::requireOnce("modules/comment/classes/GalleryCommentHelper.class"); + list (, $stats["comments"]) = g2(GalleryCommentHelper::fetchAllComments($root_album_id, 1)); + } else { + $stats["comments"] = 0; + } + + if (g2_import::g2_module_active("tags") && module::is_active("tag")) { + $result = + g2($gallery->search("SELECT COUNT(DISTINCT([TagItemMap::itemId])) FROM [TagItemMap]")) + ->nextResult(); + $stats["tags"] = $result[0]; + } else { + $stats["tags"] = 0; + } + + return $stats; + } + + /** + * Return a set of statistics about the number of users, groups, albums, photos, movies and + * comments already imported into the Gallery 3 instance. + */ + static function g3_stats() { + $g3_stats = array( + "album" => 0, "comment" => 0, "item" => 0, "user" => 0, "group" => 0, "tag" => 0); + foreach (db::build() + ->select("resource_type") + ->select(array("C" => 'COUNT("*")')) + ->from("g2_maps") + ->where("resource_type", "IN", array("album", "comment", "item", "user", "group")) + ->group_by("resource_type") + ->execute() as $row) { + $g3_stats[$row->resource_type] = $row->C; + } + return $g3_stats; + } + + /** + * Import a single group. + */ + static function import_group(&$queue) { + $messages = array(); + $g2_group_id = array_shift($queue); + if (self::map($g2_group_id)) { + return; + } + + try { + $g2_group = g2(GalleryCoreApi::loadEntitiesById($g2_group_id)); + } catch (Exception $e) { + throw new G2_Import_Exception( + t("Failed to import Gallery 2 group with id: %id,", + array("id" => $g2_group_id)), + $e); + } + + switch ($g2_group->getGroupType()) { + case GROUP_NORMAL: + try { + $group = identity::create_group($g2_group->getGroupName()); + $messages[] = t("Group '%name' was imported", + array("name" => $g2_group->getGroupname())); + } catch (Exception $e) { + // Did it fail because of a duplicate group name? + $group = identity::lookup_group_by_name($g2_group->getGroupname()); + if ($group) { + $messages[] = t("Group '%name' was mapped to the existing group group of the same name.", + array("name" => $g2_group->getGroupname())); + } else { + throw new G2_Import_Exception( + t("Failed to import group '%name'", + array("name" => $g2_group->getGroupname())), + $e); + } + } + + break; + + case GROUP_ALL_USERS: + $group = identity::registered_users(); + $messages[] = t("Group 'Registered' was converted to '%name'", array("name" => $group->name)); + break; + + case GROUP_SITE_ADMINS: + $messages[] = t("Group 'Admin' does not exist in Gallery 3, skipping"); + break; // This is not a group in G3 + + case GROUP_EVERYBODY: + $group = identity::everybody(); + $messages[] = t("Group 'Everybody' was converted to '%name'", array("name" => $group->name)); + break; + } + + if (isset($group)) { + self::set_map($g2_group->getId(), $group->id, "group"); + } + + return $messages; + } + + /** + * Import a single user. + */ + static function import_user(&$queue) { + $messages = array(); + $g2_user_id = array_shift($queue); + if (self::map($g2_user_id)) { + return t("User with id: %id already imported, skipping", + array("id" => $g2_user_id)); + } + + if (g2(GalleryCoreApi::isAnonymousUser($g2_user_id))) { + self::set_map($g2_user_id, identity::guest()->id, "group"); + return t("Skipping anonymous user"); + } + + $g2_admin_group_id = + g2(GalleryCoreApi::getPluginParameter("module", "core", "id.adminGroup")); + try { + $g2_user = g2(GalleryCoreApi::loadEntitiesById($g2_user_id)); + } catch (Exception $e) { + throw new G2_Import_Exception( + t("Failed to import Gallery 2 user with id: %id\n%exception", + array("id" => $g2_user_id, "exception" => (string)$e)), + $e); + } + $g2_groups = g2(GalleryCoreApi::fetchGroupsForUser($g2_user->getId())); + + $user = identity::lookup_user_by_name($g2_user->getUsername()); + if ($user) { + $messages[] = t("Loaded existing user: '%name'.", array("name" => $user->name)); + } else { + $email = $g2_user->getEmail(); + if (empty($email) || !valid::email($email)) { + $email = "unknown@unknown.com"; + } + try { + $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(), $email); + } catch (Exception $e) { + throw new G2_Import_Exception( + t("Failed to create user: '%name' (id: %id)", + array("name" => $g2_user->getUserName(), "id" => $g2_user_id)), + $e, $messages); + } + 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(); + } + $messages[] = t("Created user: '%name'.", array("name" => $user->name)); + if ($email == "unknown@unknown.com") { + $messages[] = t("Fixed invalid email (was '%invalid_email')", + array("invalid_email" => $g2_user->getEmail())); + } + } + + $user->locale = $g2_user->getLanguage(); + foreach ($g2_groups as $g2_group_id => $g2_group_name) { + if ($g2_group_id == $g2_admin_group_id) { + $user->admin = true; + $messages[] = t("Added 'admin' flag to user"); + } else { + $group = identity::lookup_group(self::map($g2_group_id)); + $user->add($group); + $messages[] = t("Added user to group '%group'.", array("group" => $group->name)); + } + } + + try { + $user->save(); + self::set_map($g2_user->getId(), $user->id, "user"); + } catch (Exception $e) { + throw new G2_Import_Exception( + t("Failed to create user: '%name'", array("name" => $user->name)), + $e, $messages); + } + + return $messages; + } + + /** + * Import a single album. + */ + static function import_album(&$queue) { + // The queue is a set of nested associative arrays where the key is the album id and the + // value is an array of similar arrays. We'll do a breadth first tree traversal using the + // queue to keep our state. Doing it breadth first means that the parent will be created by + // the time we get to the child. + + // Dequeue the current album and enqueue its children + list($g2_album_id, $children) = each($queue); + unset($queue[$g2_album_id]); + foreach ($children as $key => $value) { + $queue[$key] = $value; + } + + if (self::map($g2_album_id)) { + return; + } + + try { + // Load the G2 album item, and figure out its parent in G3. + $g2_album = g2(GalleryCoreApi::loadEntitiesById($g2_album_id)); + } catch (Exception $e) { + return t("Failed to load Gallery 2 album with id: %id\n%exception", + array("id" => $g2_album_id, "exception" => (string)$e)); + } + + if ($g2_album->getParentId() == null) { + $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()); + + g2_import::set_album_values($album, $g2_album); + + try { + $album->save(); + } catch (Exception $e) { + throw new G2_Import_Exception( + t("Failed to import Gallery 2 album with id %id and name %name.", + array("id" => $g2_album_id, "name" => $album->name)), + $e); + } + + 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); + } + + /** + * Transfer over all the values from a G2 album to a G3 album. + */ + static function set_album_values($album, $g2_album) { + $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()); + try { + $album->view_count = (int) g2(GalleryCoreApi::fetchItemViewCount($g2_album->getId())); + } catch (Exception $e) { + // @todo log + $album->view_count = 0; + } + $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( + 1 => "ASC", + ORDER_ASCENDING => "ASC", + ORDER_DESCENDING => "DESC"); + // G2 sorts can either be <sort> or <presort>|<sort>. Right now we can't + // map presorts so ignore them. + $g2_order = explode("|", $g2_album->getOrderBy() . ""); + $g2_order = end($g2_order); + if (empty($g2_order)) { + $g2_order = g2(GalleryCoreApi::getPluginParameter('module', 'core', 'default.orderBy')); + } + $g2_order_direction = explode("|", $g2_album->getOrderDirection() . ""); + $g2_order_direction = $g2_order_direction[0]; + if (empty($g2_order_direction)) { + $g2_order_direction = + g2(GalleryCoreApi::getPluginParameter('module', 'core', 'default.orderDirection')); + } + if (array_key_exists($g2_order, $order_map)) { + $album->sort_column = $order_map[$g2_order]; + $album->sort_order = $direction_map[$g2_order_direction]; + } + } + + /** + * Set the highlight properly for a single album + */ + static function set_album_highlight(&$queue) { + // Dequeue the current album and enqueue its children + list($g2_album_id, $children) = each($queue); + unset($queue[$g2_album_id]); + if (!empty($children)) { + foreach ($children as $key => $value) { + $queue[$key] = $value; + } + } + + $messages = array(); + $g3_album_id = self::map($g2_album_id); + if (!$g3_album_id) { + return t("Album with id: %id not imported", array("id" => $g3_album_id)); + } + + $table = g2(GalleryCoreApi::fetchThumbnailsByItemIds(array($g2_album_id))); + if (isset($table[$g2_album_id])) { + // Backtrack the source id to an item + $orig_g2_source = $g2_source = $table[$g2_album_id]; + while (GalleryUtilities::isA($g2_source, "GalleryDerivative")) { + $g2_source = g2(GalleryCoreApi::loadEntitiesById($g2_source->getDerivativeSourceId())); + } + $item_id = self::map($g2_source->getId()); + if ($item_id) { + $item = ORM::factory("item", $item_id); + $g3_album = ORM::factory("item", $g3_album_id); + $g3_album->album_cover_item_id = $item->id; + $g3_album->thumb_dirty = 1; + try { + $g3_album->view_count = (int) g2(GalleryCoreApi::fetchItemViewCount($g2_album_id)); + } catch (Exception $e) { + $g3_album->view_count = 0; + } + try { + $g3_album->save(); + graphics::generate($g3_album); + } catch (Exception $e) { + return (string) new G2_Import_Exception( + t("Failed to generate an album highlight for album '%name'.", + array("name" => $g3_album->name)), + $e); + } + + self::set_map( + $orig_g2_source->getId(), $g3_album->id, + "thumbnail", + self::g2_url(array("view" => "core.DownloadItem", "itemId" => $orig_g2_source->getId()))); + } + } + } + + /** + * Import a single photo or movie. + */ + static function import_item(&$queue) { + $g2_item_id = array_shift($queue); + + if (self::map($g2_item_id)) { + return; + } + + try { + self::$current_g2_item = $g2_item = g2(GalleryCoreApi::loadEntitiesById($g2_item_id)); + $g2_path = g2($g2_item->fetchPath()); + } catch (Exception $e) { + return t("Failed to import Gallery 2 item with id: %id\n%exception", + array("id" => $g2_item_id, "exception" => (string)$e)); + } + + $parent = ORM::factory("item", self::map($g2_item->getParentId())); + + $g2_type = $g2_item->getEntityType(); + $corrupt = 0; + if (!file_exists($g2_path)) { + // If the Gallery 2 source image isn't available, this operation is going to fail. That can + // happen in cases where there's corruption in the source Gallery 2. In that case, fall + // back on using a broken image. It's important that we import *something* otherwise + // anything that refers to this item in Gallery 2 will have a dangling pointer in Gallery 3 + // + // Note that this will change movies to be photos, if there's a broken movie. Hopefully + // this case is rare enough that we don't need to take any heroic action here. + g2_import::log( + t("%path missing in import; replacing it with a placeholder", array("path" => $g2_path))); + $g2_path = MODPATH . "g2_import/data/broken-image.gif"; + $g2_type = "GalleryPhotoItem"; + $corrupt = 1; + } + + $messages = array(); + switch ($g2_type) { + case "GalleryPhotoItem": + if (!in_array($g2_item->getMimeType(), array("image/jpeg", "image/gif", "image/png"))) { + Kohana_Log::add("alert", "$g2_path is an unsupported image type; using a placeholder gif"); + $messages[] = t("'%path' is an unsupported image type, using a placeholder", + array("path" => $g2_path)); + $g2_path = MODPATH . "g2_import/data/broken-image.gif"; + $corrupt = 1; + } + try { + $item = ORM::factory("item"); + $item->type = "photo"; + $item->parent_id = $parent->id; + $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(); + + // If the item has a preferred derivative with a rotation, then rotate this image + // accordingly. Should we obey scale rules as well? I vote no because rotation is less + // destructive -- you lose too much data from scaling. + $g2_preferred = g2(GalleryCoreApi::fetchPreferredSource($g2_item)); + if ($g2_preferred && $g2_preferred instanceof GalleryDerivative) { + if (preg_match("/rotate\|(-?\d+)/", $g2_preferred->getDerivativeOperations(), $matches)) { + $tmpfile = tempnam(TMPPATH, "rotate"); + gallery_graphics::rotate($item->file_path(), $tmpfile, array("degrees" => $matches[1]), $item); + $item->set_data_file($tmpfile); + $item->save(); + unlink($tmpfile); + } + } + } catch (Exception $e) { + $exception_info = (string) new G2_Import_Exception( + t("Corrupt image '%path'", array("path" => $g2_path)), + $e, $messages); + Kohana_Log::add("alert", "Corrupt image $g2_path\n" . $exception_info); + $messages[] = $exception_info; + $corrupt = 1; + $item = null; + } + break; + + case "GalleryMovieItem": + // @todo we should transcode other types into FLV + if (in_array($g2_item->getMimeType(), array("video/mp4", "video/x-flv"))) { + try { + $item = ORM::factory("item"); + $item->type = "movie"; + $item->parent_id = $parent->id; + $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(); + } catch (Exception $e) { + $exception_info = (string) new G2_Import_Exception( + t("Corrupt movie '%path'", array("path" => $g2_path)), + $e, $messages); + Kohana_Log::add("alert", "Corrupt movie $g2_path\n" . $exception_info); + $messages[] = $exception_info; + $corrupt = 1; + $item = null; + } + } else { + Kohana_Log::add("alert", "$g2_path is an unsupported movie type"); + $messages[] = t("'%path' is an unsupported movie type", array("path" => $g2_path)); + $corrupt = 1; + } + + break; + + default: + // Ignore + break; + } + + if (!empty($item)) { + self::import_keywords_as_tags($g2_item->getKeywords(), $item); + } + + $g2_item_url = self::g2_url(array("view" => "core.ShowItem", "itemId" => $g2_item->getId())); + if (isset($item)) { + try { + $item->view_count = (int) g2(GalleryCoreApi::fetchItemViewCount($g2_item_id)); + } catch (Exception $e) { + $view_count = 1; + } + $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) { + switch ($derivative->getDerivativeType()) { + case DERIVATIVE_TYPE_IMAGE_THUMBNAIL: $resource_type = "thumbnail"; break; + case DERIVATIVE_TYPE_IMAGE_RESIZE: $resource_type = "resize"; break; + case DERIVATIVE_TYPE_IMAGE_PREFERRED: $resource_type = "full"; break; + } + + self::set_map( + $derivative->getId(), $item->id, + $resource_type, + self::g2_url(array("view" => "core.DownloadItem", "itemId" => $derivative->getId()))); + } + } + } + + if ($corrupt) { + if (!empty($item)) { + $title = $g2_item->getTitle(); + $title or $title = $g2_item->getPathComponent(); + $messages[] = + t("<a href=\"%g2_url\">%title</a> from Gallery 2 could not be processed; (imported as <a href=\"%g3_url\">%title</a>)", + array("g2_url" => $g2_item_url, + "g3_url" => $item->url(), + "title" => $title)); + } else { + $messages[] = + t("<a href=\"%g2_url\">%title</a> from Gallery 2 could not be processed", + array("g2_url" => $g2_item_url, "title" => $g2_item->getTitle())); + } + } + + self::$current_g2_item = null; + return $messages; + } + + /** + * g2 encoded'&', '"', '<' and '>' as '&', '"', '<' and '>' respectively. + * This function undoes that encoding. + */ + private static function _decode_html_special_chars($value) { + return str_replace(array("&", """, "<", ">"), + 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. + */ + static function import_comment(&$queue) { + $g2_comment_id = array_shift($queue); + + try { + $g2_comment = g2(GalleryCoreApi::loadEntitiesById($g2_comment_id)); + } catch (Exception $e) { + return t("Failed to load Gallery 2 comment with id: %id\%exception", + array("id" => $g2_comment_id, "exception" => (string)$e)); + } + + if ($id = self::map($g2_comment->getId())) { + if (ORM::factory("comment", $id)->loaded()) { + // Already imported and still exists + return; + } + // This comment was already imported, but now it no longer exists. Import it again, per + // ticket #1736. + } + + $item_id = self::map($g2_comment->getParentId()); + if (empty($item_id)) { + // Item was not mapped. + return; + } + + $text = join("\n", array($g2_comment->getSubject(), $g2_comment->getComment())); + $text = html_entity_decode($text); + + // Just import the fields we know about. Do this outside of the comment API for now so that + // we don't trigger spam filtering events + $comment = ORM::factory("comment"); + $comment->author_id = self::map($g2_comment->getCommenterId()); + $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->guest_email = "unknown@nobody.com"; + } + $comment->item_id = $item_id; + $comment->text = self::_transform_bbcode($text); + $comment->state = "published"; + $comment->server_http_host = $g2_comment->getHost(); + try { + $comment->save(); + } catch (Exception $e) { + return (string) new G2_Import_Exception( + t("Failed to import comment with id: %id.", + array("id" => $g2_comment_id)), + $e); + } + + self::set_map($g2_comment->getId(), $comment->id, "comment"); + + // Backdate the creation date. We can't do this at creation time because + // Comment_Model::save() will override it. Leave the updated date alone + // so that if the comments get marked as spam, they don't immediately get + // flushed (see ticket #1736) + db::update("comments") + ->set("created", $g2_comment->getDate()) + ->where("id", "=", $comment->id) + ->execute(); + } + + /** + * Import all the tags for a single item + */ + static function import_tags_for_item(&$queue) { + if (!module::is_active("tag")) { + return t("Gallery 3 tag module is inactive, no tags will be imported"); + } + + GalleryCoreApi::requireOnce("modules/tags/classes/TagsHelper.class"); + $g2_item_id = array_shift($queue); + $g3_item = ORM::factory("item", self::map($g2_item_id)); + if (!$g3_item->loaded()) { + return; + } + + try { + $tag_names = array_values(g2(TagsHelper::getTagsByItemId($g2_item_id))); + } catch (Exception $e) { + return t("Failed to import Gallery 2 tags for item with id: %id\n%exception", + array("id" => $g2_item_id, "exception" => (string)$e)); + } + + foreach ($tag_names as $tag_name) { + tag::add($g3_item, $tag_name); + } + + // Tag operations are idempotent so we don't need to map them. Which is good because we don't + // have an id for each individual tag mapping anyway so it'd be hard to set up the mapping. + } + + static function import_keywords_as_tags($keywords, $item) { + // Keywords in G2 are free form. So we don't know what our user used as a separator. Try to + // be smart about it. If we see a comma or a semicolon, expect the keywords to be separated + // by that delimeter. Otherwise, use space as the delimiter. + if (strpos($keywords, ";")) { + $delim = ";"; + } else if (strpos($keywords, ",")) { + $delim = ","; + } else { + $delim = " "; + } + + foreach (preg_split("/$delim/", $keywords) as $keyword) { + $keyword = trim($keyword); + if ($keyword) { + tag::add($item, $keyword); + } + } + } + + /** + * If the thumbnails and resizes created for the Gallery 2 photo match the dimensions of the + * ones we expect to create for Gallery 3, then copy the files over instead of recreating them. + */ + static function copy_matching_thumbnails_and_resizes($item) { + // We only operate on items that are being imported + if (empty(self::$current_g2_item)) { + return; + } + + // Precaution: if the Gallery 2 item was watermarked, or we have the Gallery 3 watermark module + // active then we'd have to do something a lot more sophisticated here. For now, just skip + // this step in those cases. + // @todo we should probably use an API here, eventually. + if (module::is_active("watermark") && module::get_var("watermark", "name")) { + return; + } + + // For now just do the copy for photos and movies. Albums are tricky because we're may not + // yet be setting their album cover properly. + // @todo implement this for albums also + if (!$item->is_movie() && !$item->is_photo()) { + return; + } + + $g2_item_id = self::$current_g2_item->getId(); + $derivatives = g2(GalleryCoreApi::fetchDerivativesByItemIds(array($g2_item_id))); + + $target_thumb_size = module::get_var("gallery", "thumb_size"); + $target_resize_size = module::get_var("gallery", "resize_size"); + if (!empty($derivatives[$g2_item_id])) { + foreach ($derivatives[$g2_item_id] as $derivative) { + if ($derivative->getPostFilterOperations()) { + // Let's assume for now that this is a watermark operation, which we can't handle. + continue; + } + + if ($derivative->getDerivativeType() == DERIVATIVE_TYPE_IMAGE_THUMBNAIL && + $item->thumb_dirty && + ($derivative->getWidth() == $target_thumb_size || + $derivative->getHeight() == $target_thumb_size)) { + if (@copy(g2($derivative->fetchPath()), $item->thumb_path())) { + $item->thumb_height = $derivative->getHeight(); + $item->thumb_width = $derivative->getWidth(); + $item->thumb_dirty = 0; + } + } + + if ($derivative->getDerivativeType() == DERIVATIVE_TYPE_IMAGE_RESIZE && + $item->resize_dirty && + ($derivative->getWidth() == $target_resize_size || + $derivative->getHeight() == $target_resize_size)) { + if (@copy(g2($derivative->fetchPath()), $item->resize_path())) { + $item->resize_height = $derivative->getHeight(); + $item->resize_width = $derivative->getWidth(); + $item->resize_dirty = 0; + } + } + } + } + try { + $item->save(); + } catch (Exception $e) { + return (string) new G2_Import_Exception( + t("Failed to copy thumbnails and resizes for item '%name' (Gallery 2 id: %id)", + array("name" => $item->name, "id" => $g2_item_id)), + $e); + } + } + + /** + * Figure out the most common resize and thumb sizes in Gallery 2 so that we can tell the admin + * what theme settings to set to make the import go faster. If we match up the sizes then we + * can just copy over derivatives instead of running graphics toolkit operations. + */ + static function common_sizes() { + global $gallery; + foreach (array("resize" => DERIVATIVE_TYPE_IMAGE_RESIZE, + "thumb" => DERIVATIVE_TYPE_IMAGE_THUMBNAIL) as $type => $g2_enum) { + $results = g2($gallery->search( + "SELECT COUNT(*) AS c, [GalleryDerivativeImage::width] " . + "FROM [GalleryDerivativeImage], [GalleryDerivative] " . + "WHERE [GalleryDerivativeImage::id] = [GalleryDerivative::id] " . + " AND [GalleryDerivative::derivativeType] = ? " . + " AND [GalleryDerivativeImage::width] >= [GalleryDerivativeImage::height] " . + "GROUP BY [GalleryDerivativeImage::width] " . + "ORDER by c DESC", + array($g2_enum), + array("limit" => array(1)))); + $row = $results->nextResult(); + $sizes[$type] = array("size" => $row[1], "count" => $row[0]); + + $results = g2($gallery->search( + "SELECT COUNT(*) AS c, [GalleryDerivativeImage::height] " . + "FROM [GalleryDerivativeImage], [GalleryDerivative] " . + "WHERE [GalleryDerivativeImage::id] = [GalleryDerivative::id] " . + " AND [GalleryDerivative::derivativeType] = ? " . + " AND [GalleryDerivativeImage::height] > [GalleryDerivativeImage::width] " . + "GROUP BY [GalleryDerivativeImage::height] " . + "ORDER by c DESC", + array($g2_enum), + array("limit" => array(1)))); + $row = $results->nextResult(); + // Compare the counts. If the best fitting height does not match the best fitting width, + // then pick the one with the largest count. Otherwise, sum them. + if ($sizes[$type]["size"] != $row[1]) { + if ($row[0] > $sizes[$type]["count"]) { + $sizes[$type] = array("size" => $row[1], "count" => $row[0]); + } + } else { + $sizes[$type]["count"] += $row[0]; + } + + $results = g2($gallery->search( + "SELECT COUNT(*) FROM [GalleryDerivative] WHERE [GalleryDerivative::derivativeType] = ?", + array($g2_enum))); + $row = $results->nextResult(); + $sizes[$type]["total"] = $row[0]; + } + + return $sizes; + } + + /** + * Sensibly concatenate Gallery 2 summary and descriptions into a single field. + */ + static function extract_description($g2_item) { + // If the summary is a subset of the description just import the description, else import both. + $g2_summary = $g2_item->getSummary(); + $g2_description = $g2_item->getDescription(); + if (!$g2_summary || + $g2_summary == $g2_description || + strstr($g2_description, $g2_summary) !== false) { + $description = $g2_description; + } else { + $description = $g2_summary . " " . $g2_description; + } + return self::_transform_bbcode($description); + } + + static $bbcode_mappings = array( + "#\\[b\\](.*?)\\[/b\\]#" => "<b>$1</b>", + "#\\[i\\](.*?)\\[/i\\]#" => "<i>$1</i>", + "#\\[u\\](.*?)\\[/u\\]#" => "<u>$1</u>", + "#\\[s\\](.*?)\\[/s\\]#" => "<s>$1</s>", + "#\\[url\\](.*?)\[/url\\]#" => "<a href=\"$1\">$1</a>", + "#\\[url=(.*?)\\](.*?)\[/url\\]#" => "<a href=\"$1\">$2</a>", + "#\\[img\\](.*?)\\[/img\\]#" => "<img src=\"$1\"/>", + "#\\[quote\\](.*?)\\[/quote\\]#" => "<blockquote><p>$1</p></blockquote>", + "#\\[code\\](.*?)\\[/code\\]#" => "<pre>$1</pre>", + "#\\[size=([^\\[]*)\\]([^\\[]*)\\[/size\\]#" => "<font size=\"$1\">$2</font>", + "#\\[color=([^\\[]*)\\]([^\\[]*)\\[/color\\]#" => "<font color=\"$1\">$2/font>", + "#\\[ul\\](.*?)\\/ul\\]#" => "<ul>$1</ul>", + "#\\[li\\](.*?)\\[/li\\]#" => "<li>$1</li>", + ); + private static function _transform_bbcode($text) { + if (strpos($text, "[") !== false) { + $text = preg_replace(array_keys(self::$bbcode_mappings), array_values(self::$bbcode_mappings), + $text); + } + return $text; + } + + /** + * Get a set of photo and movie ids from Gallery 2 greater than $min_id. We use this to get the + * next chunk of photos/movies to import. + */ + static function get_item_ids($min_id) { + global $gallery; + + $ids = array(); + $results = g2($gallery->search( + "SELECT [GalleryItem::id] " . + "FROM [GalleryEntity], [GalleryItem] " . + "WHERE [GalleryEntity::id] = [GalleryItem::id] " . + "AND [GalleryEntity::entityType] IN ('GalleryPhotoItem', 'GalleryMovieItem') " . + "AND [GalleryItem::id] > ? " . + "ORDER BY [GalleryItem::id] ASC", + array($min_id), + array("limit" => array("count" => 100)))); + while ($result = $results->nextResult()) { + $ids[] = $result[0]; + } + return $ids; + } + + /** + * Get a set of comment ids from Gallery 2 greater than $min_id. We use this to get the + * next chunk of comments to import. + */ + static function get_comment_ids($min_id) { + global $gallery; + + $ids = array(); + $results = g2($gallery->search( + "SELECT [GalleryComment::id] " . + "FROM [GalleryComment] " . + "WHERE [GalleryComment::publishStatus] = 0 " . // 0 == COMMENT_PUBLISH_STATUS_PUBLISHED + "AND [GalleryComment::id] > ? " . + "ORDER BY [GalleryComment::id] ASC", + array($min_id), + array("limit" => array("count" => 100)))); + while ($result = $results->nextResult()) { + $ids[] = $result[0]; + } + return $ids; + } + + /** + * Get a set of comment ids from Gallery 2 greater than $min_id. We use this to get the + * next chunk of comments to import. + */ + static function get_tag_item_ids($min_id) { + global $gallery; + + $ids = array(); + $results = g2($gallery->search( + "SELECT DISTINCT([TagItemMap::itemId]) FROM [TagItemMap] " . + "WHERE [TagItemMap::itemId] > ? " . + "ORDER BY [TagItemMap::itemId] ASC", + array($min_id), + array("limit" => array("count" => 100)))); + while ($result = $results->nextResult()) { + $ids[] = $result[0]; + } + return $ids; + } + + /** + * Get a set of user ids from Gallery 2 greater than $min_id. We use this to get the + * next chunk of users to import. + */ + static function get_user_ids($min_id) { + global $gallery; + + $ids = array(); + $results = g2($gallery->search( + "SELECT [GalleryUser::id] " . + "FROM [GalleryUser] " . + "WHERE [GalleryUser::id] > ? " . + "ORDER BY [GalleryUser::id] ASC", + array($min_id), + array("limit" => array("count" => 100)))); + while ($result = $results->nextResult()) { + $ids[] = $result[0]; + } + return $ids; + } + + /** + * Get a set of group ids from Gallery 2 greater than $min_id. We use this to get the + * next chunk of groups to import. + */ + static function get_group_ids($min_id) { + global $gallery; + + $ids = array(); + $results = g2($gallery->search( + "SELECT [GalleryGroup::id] " . + "FROM [GalleryGroup] " . + "WHERE [GalleryGroup::id] > ? " . + "ORDER BY [GalleryGroup::id] ASC", + array($min_id), + array("limit" => array("count" => 100)))); + while ($result = $results->nextResult()) { + $ids[] = $result[0]; + } + return $ids; + } + + /** + * Look in our map to find the corresponding Gallery 3 id for the given Gallery 2 id. + */ + static function map($g2_id) { + if (!array_key_exists($g2_id, self::$map)) { + $g2_map = ORM::factory("g2_map")->where("g2_id", "=", $g2_id)->find(); + self::$map[$g2_id] = $g2_map->loaded() ? $g2_map->g3_id : null; + } + + return self::$map[$g2_id]; + } + + /** + * Associate a Gallery 2 id with a Gallery 3 item id. + */ + static function set_map($g2_id, $g3_id, $resource_type, $g2_url=null) { + self::clear_map($g2_id, $resource_type); + $g2_map = ORM::factory("g2_map"); + $g2_map->g3_id = $g3_id; + $g2_map->g2_id = $g2_id; + $g2_map->resource_type = $resource_type; + + if (strpos($g2_url, self::$g2_base_url) === 0) { + $g2_url = substr($g2_url, strlen(self::$g2_base_url)); + } + + $g2_map->g2_url = $g2_url; + $g2_map->save(); + self::$map[$g2_id] = $g3_id; + } + + /** + * Remove all map entries associated with the given Gallery 2 id. + */ + static function clear_map($g2_id, $resource_type) { + db::build() + ->delete("g2_maps") + ->where("g2_id", "=", $g2_id) + ->where("resource_type", "=", $resource_type) + ->execute(); + } + + static function log($msg) { + message::warning($msg); + Kohana_Log::add("alert", $msg); + } + + static function g2_url($params) { + global $gallery; + return $gallery->getUrlGenerator()->generateUrl( + $params, + array("forceSessionId" => false, + "htmlEntities" => false, + "urlEncode" => false, + "useAuthToken" => false)); + } + + static function lower_error_reporting() { + // Gallery 2 was not designed to run in E_STRICT mode and will barf out errors. So dial down + // the error reporting when we make G2 calls. + self::$error_reporting = error_reporting(error_reporting() & ~E_STRICT); + } + + static function restore_error_reporting() { + error_reporting(self::$error_reporting); + } +} + +/** + * Wrapper around Gallery 2 calls. We expect the first response to be a GalleryStatus object. If + * it's not null, then throw an exception. Strip the GalleryStatus object out of the result and + * if there's only an array of 1 return value, turn it into a scalar. This allows us to simplify + * this pattern: + * list ($ret, $foo) = GalleryCoreApi::someCall(); + * if ($ret) { handle_error(); } + * + * to: + * $foo = g2(GalleryCoreApi::someCall()); + */ +function g2() { + $args = func_get_arg(0); + $ret = is_array($args) ? array_shift($args) : $args; + if ($ret) { + Kohana_Log::add("error", "Gallery 2 call failed with: " . $ret->getAsText()); + throw new Exception("@todo G2_FUNCTION_FAILED"); + } + if (count($args) == 1) { + return $args[0]; + } else { + return $args; + } +} diff --git a/modules/g2_import/helpers/g2_import_event.php b/modules/g2_import/helpers/g2_import_event.php new file mode 100644 index 0000000..b985281 --- /dev/null +++ b/modules/g2_import/helpers/g2_import_event.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class g2_import_event_Core { + static function item_deleted($item) { + db::build() + ->delete("g2_maps") + ->where("g3_id", "=", $item->id) + ->execute(); + } + + static function item_created($item) { + g2_import::copy_matching_thumbnails_and_resizes($item); + } + + static function admin_menu($menu, $theme) { + $menu + ->get("settings_menu") + ->append(Menu::factory("link") + ->id("g2_import") + ->label(t("Gallery 2 import")) + ->url(url::site("admin/g2_import"))); + } +} diff --git a/modules/g2_import/helpers/g2_import_installer.php b/modules/g2_import/helpers/g2_import_installer.php new file mode 100644 index 0000000..c756981 --- /dev/null +++ b/modules/g2_import/helpers/g2_import_installer.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class g2_import_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {g2_maps} ( + `id` int(9) NOT NULL auto_increment, + `g2_id` int(9) NOT NULL, + `g3_id` int(9) NOT NULL, + `g2_url` varchar(255) default NULL, + `resource_type` varchar(64) default NULL, + PRIMARY KEY (`id`), + KEY `g2_url` (`g2_url`), + KEY `g2_id` (`g2_id`)) + DEFAULT CHARSET=utf8;"); + + mkdir(VARPATH . "modules/g2_import"); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + $db->query("ALTER TABLE {g2_maps} ADD COLUMN `g2_url` VARCHAR(255)"); + $db->query("ALTER TABLE {g2_maps} ADD COLUMN `resource_type` VARCHAR(64)"); + $db->query("ALTER TABLE {g2_maps} ADD KEY `g2_url` (`g2_url`)"); + module::set_version("g2_import", $version = 2); + } + } + + static function uninstall() { + @dir::unlink(VARPATH . "modules/g2_import"); + } +} diff --git a/modules/g2_import/helpers/g2_import_task.php b/modules/g2_import/helpers/g2_import_task.php new file mode 100644 index 0000000..07eacc4 --- /dev/null +++ b/modules/g2_import/helpers/g2_import_task.php @@ -0,0 +1,225 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class 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(); + } + } + g2_import::restore_error_reporting(); + + if (g2_import::is_initialized()) { + return array(Task_Definition::factory() + ->callback("g2_import_task::import") + ->name(t("Import from Gallery 2")) + ->description( + t("Gallery %version detected", array("version" => $version))) + ->severity(log::SUCCESS)); + } + + return array(); + } + + static function import($task) { + g2_import::lower_error_reporting(); + + $start = microtime(true); + g2_import::init(); + + $stats = $task->get("stats"); + $done = $task->get("done"); + $total = $task->get("total"); + $completed = $task->get("completed"); + $mode = $task->get("mode"); + $queue = $task->get("queue"); + if (!isset($mode)) { + $stats = g2_import::g2_stats(); + $stats["items"] = $stats["photos"] + $stats["movies"]; + unset($stats["photos"]); + unset($stats["movies"]); + $stats["highlights"] = $stats["albums"]; + $task->set("stats", $stats); + + $task->set("total", $total = array_sum(array_values($stats))); + $completed = 0; + $mode = 0; + + $done = array(); + foreach (array_keys($stats) as $key) { + $done[$key] = 0; + } + $task->set("done", $done); + + // Ensure G2 ACLs are compacted to speed up import. + g2(GalleryCoreApi::compactAccessLists()); + } + + $modes = array("groups", "users", "albums", "items", "comments", "tags", "highlights", "done"); + while (!$task->done && microtime(true) - $start < 1.5) { + if ($done[$modes[$mode]] == $stats[$modes[$mode]]) { + // Nothing left to do for this mode. Advance. + $mode++; + $task->set("last_id", 0); + $queue = array(); + + // Start the loop from the beginning again. This way if we get to a mode that requires no + // actions (eg, if the G2 comments module isn't installed) we won't try to do any comments + // queries.. in the next iteration we'll just skip over that mode. + if ($modes[$mode] != "done") { + continue; + } + } + + switch($modes[$mode]) { + case "groups": + if (empty($queue)) { + $task->set("queue", $queue = g2_import::get_group_ids($task->get("last_id", 0))); + $task->set("last_id", end($queue)); + } + $log_message = g2_import::import_group($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing groups (%count of %total)", + array("count" => $done["groups"] + 1, "total" => $stats["groups"])); + break; + + case "users": + if (empty($queue)) { + $task->set("queue", $queue = g2_import::get_user_ids($task->get("last_id", 0))); + $task->set("last_id", end($queue)); + } + $log_message = g2_import::import_user($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing users (%count of %total)", + array("count" => $done["users"] + 1, "total" => $stats["users"])); + break; + + case "albums": + if (empty($queue)) { + $g2_root_id = g2(GalleryCoreApi::getDefaultAlbumId()); + $tree = g2(GalleryCoreApi::fetchAlbumTree()); + $task->set("queue", $queue = array($g2_root_id => $tree)); + + // Update the root album to reflect the Gallery2 root album. + $root_album = item::root(); + g2_import::set_album_values( + $root_album, g2(GalleryCoreApi::loadEntitiesById($g2_root_id))); + $root_album->save(); + } + $log_message = g2_import::import_album($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing albums (%count of %total)", + array("count" => $done["albums"] + 1, "total" => $stats["albums"])); + break; + + case "items": + if (empty($queue)) { + $task->set("queue", $queue = g2_import::get_item_ids($task->get("last_id", 0))); + $task->set("last_id", end($queue)); + } + $log_message = g2_import::import_item($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing photos (%count of %total)", + array("count" => $done["items"] + 1, "total" => $stats["items"])); + break; + + case "comments": + if (empty($queue)) { + $task->set("queue", $queue = g2_import::get_comment_ids($task->get("last_id", 0))); + $task->set("last_id", end($queue)); + } + $log_message = g2_import::import_comment($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing comments (%count of %total)", + array("count" => $done["comments"] + 1, "total" => $stats["comments"])); + + break; + + case "tags": + if (empty($queue)) { + $task->set("queue", $queue = g2_import::get_tag_item_ids($task->get("last_id", 0))); + $task->set("last_id", end($queue)); + } + $log_message = g2_import::import_tags_for_item($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Importing tags (%count of %total)", + array("count" => $done["tags"] + 1, "total" => $stats["tags"])); + + break; + + case "highlights": + if (empty($queue)) { + $task->set("queue", $queue = g2(GalleryCoreApi::fetchAlbumTree())); + } + $log_message = g2_import::set_album_highlight($queue); + if ($log_message) { + $task->log($log_message); + } + $task->status = t( + "Album highlights (%count of %total)", + array("count" => $done["highlights"] + 1, "total" => $stats["highlights"])); + + break; + + case "done": + $task->status = t("Import complete"); + $task->done = true; + $task->state = "success"; + break; + } + + if (!$task->done) { + $done[$modes[$mode]]++; + $completed++; + } + } + + $task->percent_complete = 100 * ($completed / $total); + $task->set("completed", $completed); + $task->set("mode", $mode); + $task->set("queue", $queue); + $task->set("done", $done); + + g2_import::restore_error_reporting(); + } +} diff --git a/modules/g2_import/libraries/G2_Import_Exception.php b/modules/g2_import/libraries/G2_Import_Exception.php new file mode 100644 index 0000000..7732cfd --- /dev/null +++ b/modules/g2_import/libraries/G2_Import_Exception.php @@ -0,0 +1,39 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * A wrapper for exceptions to report more details in case + * it's a ORM validation exception. + */ +class G2_Import_Exception extends Exception { + public function __construct($message, Exception $previous=null, $additional_messages=null) { + if ($additional_messages) { + $message .= "\n" . implode("\n", $additional_messages); + } + if ($previous && $previous instanceof ORM_Validation_Exception) { + $message .= "\nORM validation errors: " . print_r($previous->validation->errors(), true); + } + if ($previous) { + $message .= "\n" . (string) $previous; + } + // The $previous parameter is supported in PHP 5.3.0+. + parent::__construct($message); + } +}
\ No newline at end of file diff --git a/modules/g2_import/models/g2_map.php b/modules/g2_import/models/g2_map.php new file mode 100644 index 0000000..5fb566c --- /dev/null +++ b/modules/g2_import/models/g2_map.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class G2_Map_Model_Core extends ORM { +} diff --git a/modules/g2_import/module.info b/modules/g2_import/module.info new file mode 100644 index 0000000..32af27d --- /dev/null +++ b/modules/g2_import/module.info @@ -0,0 +1,7 @@ +name = "Gallery 2 Import" +description = "Import your Gallery 2 content into Gallery 3" +version = 2 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:g2_import" +discuss_url = "http://galleryproject.org/forum_module_g2_import" diff --git a/modules/g2_import/views/admin_g2_import.html.php b/modules/g2_import/views/admin_g2_import.html.php new file mode 100644 index 0000000..22e19f5 --- /dev/null +++ b/modules/g2_import/views/admin_g2_import.html.php @@ -0,0 +1,150 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= $theme->css("jquery.autocomplete.css") ?> +<?= $theme->script("jquery.autocomplete.js") ?> +<script type="text/javascript"> +$("document").ready(function() { + $("form input[name=embed_path]").gallery_autocomplete( + "<?= url::site("__ARGS__") ?>".replace("__ARGS__", "admin/g2_import/autocomplete"), + { + max: 256, + loadingClass: "g-loading-small", + }); +}); +</script> + +<div id="g-admin-g2-import" class="g-block"> + <h1> <?= t("Gallery 2 import") ?> </h1> + <p> + <?= t("Import your Gallery 2 users, photos, movies, comments and tags into your new Gallery 3 installation.") ?> + </p> + + <script type="text/javascript"> + $(document).ready(function() { + $("#g-admin-g2-import-tabs").tabs() + <? if (!isset($g2_version)): ?> + .tabs("disable", 1) + .tabs("disable", 2) + <? elseif ($g3_resource_count > .9 * $g2_resource_count): ?> + .tabs("select", 2) + <? else: ?> + .tabs("select", 1) + <? endif ?> + ; + + // Show the tabs after the page has loaded to prevent Firefox from rendering the + // unstyled page and then flashing. + $("#g-admin-g2-import-tabs").show(); + }); + </script> + <div id="g-admin-g2-import-tabs" class="g-block-content" style="display: none"> + <ul> + <li> + <a href="#g-admin-g2-import-configure"><?= t("1. Configure Gallery2 path") ?></a> + </li> + <li> + <a href="#g-admin-g2-import-import"><?= t("2. Import!") ?></a> + </li> + <li> + <a href="#g-admin-g2-import-notes"><?= t("3. After your import") ?></a> + </li> + </ul> + <div id="g-admin-g2-import-configure" class="g-block-content"> + <?= $form ?> + </div> + <div id="g-admin-g2-import-import"> + <? if (isset($g2_version)): ?> + <ul> + <li> + <?= t("Gallery version %version detected", array("version" => $g2_version)) ?> + </li> + <? if ($g2_sizes["thumb"]["size"] && $thumb_size != $g2_sizes["thumb"]["size"]): ?> + <li> + <?= t("Your most common thumbnail size in Gallery 2 is %g2_pixels pixels, but your Gallery 3 thumbnail size is set to %g3_pixels pixels. <a href=\"%url\">Using the same value</a> will speed up your import.", + array("g2_pixels" => $g2_sizes["thumb"]["size"], + "g3_pixels" => $thumb_size, + "url" => html::mark_clean(url::site("admin/theme_options")))) ?> + </li> + <? endif ?> + + <? if ($g2_sizes["resize"]["size"] && $resize_size != $g2_sizes["resize"]["size"]): ?> + <li> + <?= t("Your most common intermediate size in Gallery 2 is %g2_pixels pixels, but your Gallery 3 intermediate size is set to %g3_pixels pixels. <a href=\"%url\">Using the same value</a> will speed up your import.", + array("g2_pixels" => $g2_sizes["resize"]["size"], + "g3_pixels" => $resize_size, + "url" => html::mark_clean(url::site("admin/theme_options")))) ?> + </li> + <? endif ?> + + <li> + <? + $t = array(); + $t[] = t2("1 user", "%count users", $g2_stats["users"]); + $t[] = t2("1 group", "%count groups", $g2_stats["groups"]); + $t[] = t2("1 album", "%count albums", $g2_stats["albums"]); + $t[] = t2("1 photo", "%count photos/movies", $g2_stats["photos"] + $g2_stats["movies"]); + $t[] = t2("1 comment", "%count comments", $g2_stats["comments"]); + $t[] = t2("1 tagged photo/movie/album", "%count tagged photos/movies/albums", + $g2_stats["tags"]); + ?> + <?= t("Your Gallery 2 has the following importable data in it: %t0, %t1, %t2, %t3, %t4, %t5", + array("t0" => $t[0], "t1" => $t[1], "t2" => $t[2], + "t3" => $t[3], "t4" => $t[4], "t5" => $t[5])) ?> + </li> + + <? if ($g3_resource_count): ?> + <li> + <? + $t = array(); + $t[] = t2("1 user", "%count users", $g3_stats["user"]); + $t[] = t2("1 group", "%count groups", $g3_stats["group"]); + $t[] = t2("1 album", "%count albums", $g3_stats["album"]); + $t[] = t2("1 photo/movie", "%count photos/movies", $g3_stats["item"]); + $t[] = t2("1 comment", "%count comments", $g3_stats["comment"]); + $t[] = t2("1 tagged photo/movie/album", "%count tagged photos/movies/albums", $g3_stats["tag"]); + ?> + <?= t("It looks like you've imported the following Gallery 2 data already: %t0, %t1, %t2, %t3, %t4, %t5", + array("t0" => $t[0], "t1" => $t[1], "t2" => $t[2], + "t3" => $t[3], "t4" => $t[4], "t5" => $t[5])) ?> + </li> + <? endif ?> + </ul> + <p> + <a class="g-button g-dialog-link ui-state-default ui-corner-all" + href="<?= url::site("admin/maintenance/start/g2_import_task::import?csrf=$csrf") ?>"> + <?= t("Begin import!") ?> + </a> + </p> + <? endif ?> + </div> + <div id="g-admin-g2-import-notes" class="g-text"> + <ul> + <li> + <?= t("Gallery 3 does not support per-user / per-item permissions. <b>Review permissions!</b>") ?> + </li> + <li> + <?= t("The only supported file formats are JPG, PNG and GIF, FLV and MP4. Other formats will be skipped.") ?> + </li> + <li> + <p> + <?= t("Redirecting Gallery 2 URLs once your migration is complete. Put this block at the top of your gallery2/.htaccess file and all Gallery 2 urls will be redirected to Gallery 3") ?> + </p> + + <textarea id="g-g2-redirect-rules" rows="4" cols="60"><IfModule mod_rewrite.c> + Options +FollowSymLinks + RewriteEngine On + RewriteBase <?= html::clean(g2_import::$g2_base_url) ?> + + RewriteRule ^(.*)$ <?= url::site("g2/map?path=\$1") ?> [QSA,L,R=301] + </IfModule></textarea> + <script type="text/javascript"> + $(document).ready(function() { + $("#g-g2-redirect-rules").click(function(event) { + this.select(); + }); + }); + </script> + </li> + </ul> + </div> + </div> +</div> diff --git a/modules/gallery/config/cache.php b/modules/gallery/config/cache.php new file mode 100644 index 0000000..746e95c --- /dev/null +++ b/modules/gallery/config/cache.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* + * @package Cache + * + * Cache settings, defined as arrays, or "groups". If no group name is + * used when loading the cache library, the group named "default" will be used. + * + * Each group can be used independently, and multiple groups can be used at once. + * + * Group Options: + * driver - Cache backend driver. Kohana comes with file, database, and memcache drivers. + * > File cache is fast and reliable, but requires many filesystem lookups. + * > Database cache can be used to cache items remotely, but is slower. + * > Memcache is very high performance, but prevents cache tags from being used. + * + * params - Driver parameters, specific to each driver. + * + * lifetime - Default lifetime of caches in seconds. By default caches are stored for + * thirty minutes. Specific lifetime can also be set when creating a new cache. + * Setting this to 0 will never automatically delete caches. + * + * requests - Average number of cache requests that will processed before all expired + * caches are deleted. This is commonly referred to as "garbage collection". + * Setting this to 0 or a negative number will disable automatic garbage collection. + */ +$config["default"] = array ( + "driver" => "database", + "params" => null, + "lifetime" => 84600, + "requests" => 1000, + "prefix" => null, +); diff --git a/modules/gallery/config/cookie.php b/modules/gallery/config/cookie.php new file mode 100644 index 0000000..b164701 --- /dev/null +++ b/modules/gallery/config/cookie.php @@ -0,0 +1,48 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Domain, to restrict the cookie to a specific website domain. For security, + * you are encouraged to set this option. An empty setting allows the cookie + * to be read by any website domain. + */ +$config['domain'] = ''; + +/** + * Restrict cookies to a specific path, typically the installation directory. + */ +$config['path'] = '/'; + +/** + * Lifetime of the cookie. A setting of 0 makes the cookie active until the + * users browser is closed or the cookie is deleted. + */ +$config['expire'] = 0; + +/** + * Set the secure bit on the cookie if we're using HTTPS. + */ +$config['secure'] = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'; + +/** + * Enable this option to disable the cookie from being accessed when using a + * secure protocol. This option is only available in PHP 5.2 and above. + */ +$config['httponly'] = true;
\ No newline at end of file diff --git a/modules/gallery/config/database.php b/modules/gallery/config/database.php new file mode 100644 index 0000000..4148757 --- /dev/null +++ b/modules/gallery/config/database.php @@ -0,0 +1,23 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +if (file_exists(VARPATH . "database.php")) { + include(VARPATH . "database.php"); +} diff --git a/modules/gallery/config/locale.php b/modules/gallery/config/locale.php new file mode 100644 index 0000000..cf37de9 --- /dev/null +++ b/modules/gallery/config/locale.php @@ -0,0 +1,51 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * @package Core + * + * Default language locale name(s). + * First item must be a valid i18n directory name, subsequent items are alternative locales + * for OS's that don't support the first (e.g. Windows). The first valid locale in the array will be used. + * @see http://php.net/setlocale + */ +$config['language'] = array('en_US', 'English_United States'); + +/** + * Locale timezone. Set in 'Advanced' settings, falling back to the server's zone. + * @see http://php.net/timezones + */ +if (file_exists(VARPATH . "database.php")) { + $config['timezone'] = module::get_var("gallery", "timezone", date_default_timezone_get()); +} else { + // Gallery3 is not installed yet -- don't make module::get_var() calls. + $config['timezone'] = date_default_timezone_get(); +} + +// i18n settings + +/** + * The locale of the built-in localization messages (locale of strings in translate() calls). + * This can't be changed easily, unless all localization strings are replaced in all source files + * as well. + * Although the actual root is "en_US", the configured root is "en" that all en locales inherit the + * built-in strings. + */ +$config['root_locale'] = 'en';
\ No newline at end of file diff --git a/modules/gallery/config/log_file.php b/modules/gallery/config/log_file.php new file mode 100644 index 0000000..a79831e --- /dev/null +++ b/modules/gallery/config/log_file.php @@ -0,0 +1,29 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Message logging directory. + */ +$config['log_directory'] = VARPATH.'logs'; + +/** + * Permissions of the log file + */ +$config['posix_permissions'] = 0644;
\ No newline at end of file diff --git a/modules/gallery/config/routes.php b/modules/gallery/config/routes.php new file mode 100644 index 0000000..d1ae8bf --- /dev/null +++ b/modules/gallery/config/routes.php @@ -0,0 +1,32 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// Admin controllers are not available, except via /admin +$config["^admin_.*"] = null; + +// Redirect /form/add/admin/controller and /form/edit/admin/controller to +// admin/controller/form_(add|edit)/parms. provides the same as below for admin pages +$config["^form/(edit|add)/admin/(\w+)/(.*)$"] = "admin/$2/form_$1/$3"; + +// Redirect /form/add and /form/edit to the module/form_(add|edit)/parms. +$config["^form/(edit|add)/(\w+)/(.*)$"] = "$2/form_$1/$3"; + +// Default page is the root album +$config["_default"] = "albums"; diff --git a/modules/gallery/config/session.php b/modules/gallery/config/session.php new file mode 100644 index 0000000..7ecee58 --- /dev/null +++ b/modules/gallery/config/session.php @@ -0,0 +1,66 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * @package Session + * + * Session driver name. + */ +$config['driver'] = 'database'; + +/** + * Session storage parameter, used by drivers. + */ +$config['storage'] = ''; + +/** + * Session name. + * It must contain only alphanumeric characters and underscores. At least one letter must be present. + */ +$config['name'] = 'g3sid'; + +/** + * Session parameters to validate: user_agent, ip_address, expiration. + */ +$config['validate'] = array('user_agent', 'expiration'); + +/** + * Enable or disable session encryption. + * Note: this has no effect on the native session driver. + * Note: the cookie driver always encrypts session data. Set to TRUE for stronger encryption. + */ +$config['encryption'] = FALSE; + +/** + * Session lifetime. Number of seconds that each session will last. + * A value of 0 will keep the session active until the browser is closed (with a limit of 24h). + */ +$config['expiration'] = 604800; // 7 days + +/** + * Number of page loads before the session id is regenerated. + * A value of 0 will disable automatic session id regeneration. + */ +$config['regenerate'] = 0; + +/** + * Percentage probability that the gc (garbage collection) routine is started. + */ +$config['gc_probability'] = 2;
\ No newline at end of file diff --git a/modules/gallery/config/upload.php b/modules/gallery/config/upload.php new file mode 100644 index 0000000..a5e8797 --- /dev/null +++ b/modules/gallery/config/upload.php @@ -0,0 +1,36 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * @package Core + * + * This path is relative to your index file. Absolute paths are also supported. + */ +$config['directory'] = VARPATH.'uploads'; + +/** + * Enable or disable directory creation. + */ +$config['create_directories'] = FALSE; + +/** + * Remove spaces from uploaded filenames. + */ +$config['remove_spaces'] = FALSE;
\ No newline at end of file diff --git a/modules/gallery/config/user_agents.php b/modules/gallery/config/user_agents.php new file mode 100644 index 0000000..dcdbb73 --- /dev/null +++ b/modules/gallery/config/user_agents.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +include(SYSPATH . "config/user_agents.php"); +$config["robot"]["mj12bot"] = "MJ12bot"; +$config["robot"]["speedy spider"] = "Speedy Spider"; +$config["robot"]["baidu"] = "Baiduspider"; +$config["robot"]["bing"] = "Ezooms"; +$config["robot"]["yandex"] = "YandexBot"; diff --git a/modules/gallery/controllers/admin.php b/modules/gallery/controllers/admin.php new file mode 100644 index 0000000..b35a929 --- /dev/null +++ b/modules/gallery/controllers/admin.php @@ -0,0 +1,94 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Controller extends Controller { + private $theme; + + public function __construct($theme=null) { + if (!identity::active_user()->admin) { + if (identity::active_user()->guest) { + Session::instance()->set("continue_url", url::abs_current(true)); + url::redirect("login"); + } else { + access::forbidden(); + } + } + + parent::__construct(); + } + + public function __call($controller_name, $args) { + if (Input::instance()->get("reauth_check")) { + return self::_reauth_check(); + } + if (auth::must_reauth_for_admin_area()) { + return self::_prompt_for_reauth($controller_name, $args); + } + + if (request::method() == "post") { + access::verify_csrf(); + } + + if ($controller_name == "index") { + $controller_name = "dashboard"; + } + $controller_name = "Admin_{$controller_name}_Controller"; + if ($args) { + $method = array_shift($args); + } else { + $method = "index"; + } + + if (!class_exists($controller_name) || !method_exists($controller_name, $method)) { + throw new Kohana_404_Exception(); + } + + call_user_func_array(array(new $controller_name, $method), $args); + } + + private static function _reauth_check() { + $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"); + + $time_remaining = max($last_active_auth, $last_admin_area_activity) + + $admin_area_timeout - time(); + + $result = new stdClass(); + $result->result = "success"; + if ($time_remaining < 30) { + message::success(t("Automatically logged out of the admin area for your security")); + $result->location = url::abs_site(""); + } + + json::reply($result); + } + + private static function _prompt_for_reauth($controller_name, $args) { + if (request::method() == "get") { + // Avoid anti-phishing protection by passing the url as session variable. + Session::instance()->set("continue_url", url::abs_current(true)); + } + // Save the is_ajax value as we lose it, if set, when we redirect + Session::instance()->set("is_ajax_request", request::is_ajax()); + url::redirect("reauthenticate"); + } +} + diff --git a/modules/gallery/controllers/admin_advanced_settings.php b/modules/gallery/controllers/admin_advanced_settings.php new file mode 100644 index 0000000..267ff14 --- /dev/null +++ b/modules/gallery/controllers/admin_advanced_settings.php @@ -0,0 +1,57 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Advanced_Settings_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Advanced settings"); + $view->content = new View("admin_advanced_settings.html"); + $view->content->vars = ORM::factory("var") + ->order_by("module_name") + ->order_by("name") + ->find_all(); + print $view; + } + + public function edit($module_name, $var_name) { + if (module::is_installed($module_name)) { + $value = module::get_var($module_name, $var_name); + $form = new Forge("admin/advanced_settings/save/$module_name/$var_name", "", "post"); + $group = $form->group("edit_var")->label(t("Edit setting")); + $group->input("module_name")->label(t("Module"))->value($module_name)->disabled(1); + $group->input("var_name")->label(t("Setting"))->value($var_name)->disabled(1); + $group->textarea("value")->label(t("Value"))->value($value); + $group->submit("")->value(t("Save")); + print $form; + } + } + + public function save($module_name, $var_name) { + access::verify_csrf(); + + if (module::is_installed($module_name)) { + module::set_var($module_name, $var_name, Input::instance()->post("value")); + message::success( + t("Saved value for %var (%module_name)", + array("var" => $var_name, "module_name" => $module_name))); + + json::reply(array("result" => "success")); + } + } +} diff --git a/modules/gallery/controllers/admin_dashboard.php b/modules/gallery/controllers/admin_dashboard.php new file mode 100644 index 0000000..5317210 --- /dev/null +++ b/modules/gallery/controllers/admin_dashboard.php @@ -0,0 +1,97 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Dashboard_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Dashboard"); + $view->content = new View("admin_dashboard.html"); + $view->content->blocks = block_manager::get_html("dashboard_center"); + $view->sidebar = "<div id=\"g-admin-dashboard-sidebar\">" . + block_manager::get_html("dashboard_sidebar") . + "</div>"; + $view->content->obsolete_modules_message = module::get_obsolete_modules_message(); + print $view; + } + + public function add_block() { + access::verify_csrf(); + + $form = gallery_block::get_add_block_form(); + if ($form->validate()) { + list ($module_name, $id) = explode(":", $form->add_block->id->value); + $available = block_manager::get_available_admin_blocks(); + + if ($form->add_block->center->value) { + block_manager::add("dashboard_center", $module_name, $id); + message::success( + t("Added <b>%title</b> block to the dashboard center", + array("title" => $available["$module_name:$id"]))); + } else { + block_manager::add("dashboard_sidebar", $module_name, $id); + message::success( + t("Added <b>%title</b> to the dashboard sidebar", + array("title" => $available["$module_name:$id"]))); + } + } + url::redirect("admin/dashboard"); + } + + public function remove_block($id) { + access::verify_csrf(); + + $blocks_center = block_manager::get_active("dashboard_center"); + $blocks_sidebar = block_manager::get_active("dashboard_sidebar"); + + if (array_key_exists($id, $blocks_sidebar)) { + $deleted = $blocks_sidebar[$id]; + block_manager::remove("dashboard_sidebar", $id); + } else if (array_key_exists($id, $blocks_center)) { + $deleted = $blocks_center[$id]; + block_manager::remove("dashboard_center", $id); + } + + if (!empty($deleted)) { + $available = block_manager::get_available_admin_blocks(); + $title = $available[join(":", $deleted)]; + message::success(t("Removed <b>%title</b> block", array("title" => $title))); + } + + url::redirect("admin"); + } + + public function reorder() { + access::verify_csrf(); + + $active_set = array(); + foreach (array("dashboard_sidebar", "dashboard_center") as $location) { + foreach (block_manager::get_active($location) as $id => $info) { + $active_set[$id] = $info; + } + } + + foreach (array("dashboard_sidebar", "dashboard_center") as $location) { + $new_blocks = array(); + foreach (Input::instance()->get($location, array()) as $id) { + $new_blocks[$id] = $active_set[$id]; + } + block_manager::set_active($location, $new_blocks); + } + } +} diff --git a/modules/gallery/controllers/admin_graphics.php b/modules/gallery/controllers/admin_graphics.php new file mode 100644 index 0000000..a24486f --- /dev/null +++ b/modules/gallery/controllers/admin_graphics.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Graphics_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Graphics settings"); + $view->content = new View("admin_graphics.html"); + $view->content->tk = graphics::detect_toolkits(); + $view->content->active = module::get_var("gallery", "graphics_toolkit", "none"); + print $view; + } + + public function choose($toolkit_id) { + access::verify_csrf(); + + if ($toolkit_id != module::get_var("gallery", "graphics_toolkit")) { + $tk = graphics::detect_toolkits(); + module::set_var("gallery", "graphics_toolkit", $toolkit_id); + module::set_var("gallery", "graphics_toolkit_path", $tk->$toolkit_id->dir); + + site_status::clear("missing_graphics_toolkit"); + + $msg = t("Changed graphics toolkit to: %toolkit", array("toolkit" => $tk->$toolkit_id->name)); + message::success($msg); + log::success("graphics", $msg); + + module::event("graphics_toolkit_change", $toolkit_id); + } + + url::redirect("admin/graphics"); + } +} + diff --git a/modules/gallery/controllers/admin_languages.php b/modules/gallery/controllers/admin_languages.php new file mode 100644 index 0000000..50ddc67 --- /dev/null +++ b/modules/gallery/controllers/admin_languages.php @@ -0,0 +1,135 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Languages_Controller extends Admin_Controller { + public function index($share_translations_form=null) { + $v = new Admin_View("admin.html"); + $v->page_title = t("Languages and translations"); + $v->content = new View("admin_languages.html"); + $v->content->available_locales = locales::available(); + $v->content->installed_locales = locales::installed(); + $v->content->default_locale = module::get_var("gallery", "default_locale"); + + if (empty($share_translations_form)) { + $share_translations_form = $this->_share_translations_form(); + } + $v->content->share_translations_form = $share_translations_form; + $this->_outgoing_translations_count(); + print $v; + } + + public function save() { + access::verify_csrf(); + + $input = Input::instance(); + locales::update_installed($input->post("installed_locales")); + + $installed_locales = array_keys(locales::installed()); + $new_default_locale = $input->post("default_locale"); + if (!in_array($new_default_locale, $installed_locales)) { + if (!empty($installed_locales)) { + $new_default_locale = $installed_locales[0]; + } else { + $new_default_locale = "en_US"; + } + } + module::set_var("gallery", "default_locale", $new_default_locale); + + json::reply(array("result" => "success")); + } + + public function share() { + access::verify_csrf(); + + $form = $this->_share_translations_form(); + if (!$form->validate()) { + // Show the page with form errors + return $this->index($form); + } + + if (Input::instance()->post("share")) { + l10n_client::submit_translations(); + message::success(t("Translations submitted")); + } else { + return $this->_save_api_key($form); + } + url::redirect("admin/languages"); + } + + private function _save_api_key($form) { + $new_key = $form->sharing->api_key->value; + if ($new_key) { + list($connected, $valid) = l10n_client::validate_api_key($new_key); + if (!$valid) { + $form->sharing->api_key->add_error($connected ? "invalid" : "no_connection", 1); + } + } else { + $valid = true; + } + + if ($valid) { + $old_key = l10n_client::api_key(); + l10n_client::api_key($new_key); + if ($old_key && !$new_key) { + message::success(t("Your API key has been cleared.")); + } else if ($old_key && $new_key && $old_key != $new_key) { + message::success(t("Your API key has been changed.")); + } else if (!$old_key && $new_key) { + message::success(t("Your API key has been saved.")); + } else if ($old_key && $new_key && $old_key == $new_key) { + message::info(t("Your API key was not changed.")); + } + + log::success(t("gallery"), t("l10n_client API key changed.")); + url::redirect("admin/languages"); + } else { + // Show the page with form errors + $this->index($form); + } + } + + private function _outgoing_translations_count() { + return ORM::factory("outgoing_translation")->count_all(); + } + + private function _share_translations_form() { + $form = new Forge("admin/languages/share", "", "post", array("id" => "g-share-translations-form")); + $group = $form->group("sharing") + ->label("Translations API Key"); + $api_key = l10n_client::api_key(); + $server_link = l10n_client::server_api_key_url(); + $group->input("api_key") + ->label(empty($api_key) + ? t("This is a unique key that will allow you to send translations to the remote + server. To get your API key go to %server-link.", + array("server-link" => html::mark_clean(html::anchor($server_link)))) + : t("API key")) + ->value($api_key) + ->error_messages("invalid", t("The API key you provided is invalid.")) + ->error_messages( + "no_connection", t("Could not connect to remote server to validate the API key.")); + $group->submit("save")->value(t("Save settings")); + if ($api_key && $this->_outgoing_translations_count()) { + // TODO: UI improvement: hide API key / save button when API key is set. + $group->submit("share")->value(t("Submit translations")); + } + return $form; + } +} + diff --git a/modules/gallery/controllers/admin_maintenance.php b/modules/gallery/controllers/admin_maintenance.php new file mode 100644 index 0000000..32f2078 --- /dev/null +++ b/modules/gallery/controllers/admin_maintenance.php @@ -0,0 +1,243 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Maintenance_Controller extends Admin_Controller { + /** + * Show a list of all available, running and finished tasks. + */ + public function index() { + $query = db::build() + ->update("tasks") + ->set("state", "stalled") + ->where("done", "=", 0) + ->where("state", "<>", "stalled") + ->where(db::expr("UNIX_TIMESTAMP(NOW()) - `updated` > 15")) + ->execute(); + $stalled_count = $query->count(); + if ($stalled_count) { + log::warning("tasks", + t2("One task is stalled", + "%count tasks are stalled", + $stalled_count), + t('<a href="%url">view</a>', + array("url" => html::mark_clean(url::site("admin/maintenance"))))); + } + + $view = new Admin_View("admin.html"); + $view->page_title = t("Maintenance tasks"); + $view->content = new View("admin_maintenance.html"); + $view->content->task_definitions = task::get_definitions(); + $view->content->running_tasks = ORM::factory("task") + ->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(); + print $view; + + // Do some maintenance while we're in here + db::build() + ->delete("caches") + ->where("expiration", "<>", 0) + ->where("expiration", "<=", time()) + ->execute(); + module::deactivate_missing_modules(); + } + + /** + * Start a new task + * @param string $task_callback + */ + public function start($task_callback) { + access::verify_csrf(); + + $task = task::start($task_callback); + $view = new View("admin_maintenance_task.html"); + $view->task = $task; + + log::info("tasks", t("Task %task_name started (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor("admin/maintenance", t("maintenance"))); + print $view; + } + + /** + * Resume a stalled task + * @param string $task_id + */ + public function resume($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded()) { + throw new Exception("@todo MISSING_TASK"); + } + $view = new View("admin_maintenance_task.html"); + $view->task = $task; + + $task->log(t("Task %task_name resumed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id))); + log::info("tasks", t("Task %task_name resumed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor("admin/maintenance", t("maintenance"))); + print $view; + } + + /** + * Show the task log + * @param string $task_id + */ + public function show_log($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded()) { + throw new Exception("@todo MISSING_TASK"); + } + $view = new View("admin_maintenance_show_log.html"); + $view->task = $task; + + print $view; + } + + /** + * Save the task log + * @param string $task_id + */ + public function save_log($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded()) { + throw new Exception("@todo MISSING_TASK"); + } + + header("Content-Type: application/text"); + header("Content-Disposition: filename=gallery3_task_log.txt"); + print $task->get_log(); + } + + /** + * Cancel a task. + * @param string $task_id + */ + public function cancel($task_id) { + access::verify_csrf(); + + task::cancel($task_id); + + message::success(t("Task cancelled")); + url::redirect("admin/maintenance"); + } + + public function cancel_running_tasks() { + access::verify_csrf(); + db::build() + ->update("tasks") + ->set("done", 1) + ->set("state", "cancelled") + ->where("done", "=", 0) + ->execute(); + message::success(t("All running tasks cancelled")); + url::redirect("admin/maintenance"); + } + + /** + * Remove a task. + * @param string $task_id + */ + public function remove($task_id) { + access::verify_csrf(); + + task::remove($task_id); + + message::success(t("Task removed")); + url::redirect("admin/maintenance"); + } + + public function remove_finished_tasks() { + access::verify_csrf(); + + // Do it the long way so we can call delete and remove the cache. + $finished = ORM::factory("task") + ->where("done", "=", 1) + ->find_all(); + foreach ($finished as $task) { + task::remove($task->id); + } + message::success(t("All finished tasks removed")); + url::redirect("admin/maintenance"); + } + + /** + * Run a task. This will trigger the task to do a small amount of work, then it will report + * back with status on the task. + * @param string $task_id + */ + public function run($task_id) { + access::verify_csrf(); + + try { + $task = task::run($task_id); + } catch (Exception $e) { + Kohana_Log::add( + "error", + sprintf( + "%s in %s at line %s:\n%s", $e->getMessage(), $e->getFile(), + $e->getLine(), $e->getTraceAsString())); + throw $e; + } + + if ($task->done) { + switch ($task->state) { + case "success": + log::success("tasks", t("Task %task_name completed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor("admin/maintenance", t("maintenance"))); + message::success(t("Task completed successfully")); + break; + + case "error": + log::error("tasks", t("Task %task_name failed (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id)), + html::anchor("admin/maintenance", t("maintenance"))); + message::success(t("Task failed")); + break; + } + // Using sprintf("%F") to avoid comma as decimal separator. + json::reply(array("result" => "success", + "task" => array( + "percent_complete" => sprintf("%F", $task->percent_complete), + "status" => (string) $task->status, + "done" => (bool) $task->done), + "location" => url::site("admin/maintenance"))); + + } else { + json::reply(array("result" => "in_progress", + "task" => array( + "percent_complete" => sprintf("%F", $task->percent_complete), + "status" => (string) $task->status, + "done" => (bool) $task->done))); + } + } + + public function maintenance_mode($value) { + access::verify_csrf(); + module::set_var("gallery", "maintenance_mode", $value); + url::redirect("admin/maintenance"); + } +} diff --git a/modules/gallery/controllers/admin_modules.php b/modules/gallery/controllers/admin_modules.php new file mode 100644 index 0000000..177a925 --- /dev/null +++ b/modules/gallery/controllers/admin_modules.php @@ -0,0 +1,119 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Modules_Controller extends Admin_Controller { + public function index() { + // If modules need upgrading, this will get recreated in module::available() + site_status::clear("upgrade_now"); + + $view = new Admin_View("admin.html"); + $view->page_title = t("Modules"); + $view->content = new View("admin_modules.html"); + $view->content->available = module::available(); + $view->content->obsolete_modules_message = module::get_obsolete_modules_message(); + print $view; + } + + + public function confirm() { + access::verify_csrf(); + + $messages = array("error" => array(), "warn" => array()); + $desired_list = array(); + foreach (module::available() as $module_name => $info) { + if ($info->locked) { + continue; + } + + if ($desired = Input::instance()->post($module_name) == 1) { + $desired_list[] = $module_name; + } + if ($info->active && !$desired && module::is_active($module_name)) { + $messages = array_merge($messages, module::can_deactivate($module_name)); + } else if (!$info->active && $desired && !module::is_active($module_name)) { + $messages = array_merge($messages, module::can_activate($module_name)); + } + } + + if (empty($messages["error"]) && empty($messages["warn"])) { + $this->_do_save(); + $result["reload"] = 1; + } else { + $v = new View("admin_modules_confirm.html"); + $v->messages = $messages; + $v->modules = $desired_list; + $result["dialog"] = (string)$v; + $result["allow_continue"] = empty($messages["error"]); + } + json::reply($result); + } + + public function save() { + access::verify_csrf(); + + $this->_do_save(); + url::redirect("admin/modules"); + } + + private function _do_save() { + $changes = new stdClass(); + $changes->activate = array(); + $changes->deactivate = array(); + $activated_names = array(); + $deactivated_names = array(); + foreach (module::available() as $module_name => $info) { + if ($info->locked) { + continue; + } + + try { + $desired = Input::instance()->post($module_name) == 1; + if ($info->active && !$desired && module::is_active($module_name)) { + module::deactivate($module_name); + $changes->deactivate[] = $module_name; + $deactivated_names[] = t($info->name); + } else if (!$info->active && $desired && !module::is_active($module_name)) { + if (module::is_installed($module_name)) { + module::upgrade($module_name); + } else { + module::install($module_name); + } + module::activate($module_name); + $changes->activate[] = $module_name; + $activated_names[] = t($info->name); + } + } catch (Exception $e) { + message::warning(t("An error occurred while installing the <b>%module_name</b> module", + array("module_name" => $info->name))); + Kohana_Log::add("error", (string)$e); + } + } + + module::event("module_change", $changes); + + // @todo this type of collation is questionable from an i18n perspective + if ($activated_names) { + message::success(t("Activated: %names", array("names" => join(", ", $activated_names)))); + } + if ($deactivated_names) { + message::success(t("Deactivated: %names", array("names" => join(", ", $deactivated_names)))); + } + } +} + diff --git a/modules/gallery/controllers/admin_movies.php b/modules/gallery/controllers/admin_movies.php new file mode 100644 index 0000000..38fa44a --- /dev/null +++ b/modules/gallery/controllers/admin_movies.php @@ -0,0 +1,72 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Movies_Controller extends Admin_Controller { + public function index() { + // Print screen from new form. + $form = $this->_get_admin_form(); + $this->_print_view($form); + } + + public function save() { + access::verify_csrf(); + $form = $this->_get_admin_form(); + if ($form->validate()) { + module::set_var("gallery", "movie_allow_uploads", $form->settings->allow_uploads->value); + if ($form->settings->rebuild_thumbs->value) { + graphics::mark_dirty(true, false, "movie"); + } + // All done - redirect with message. + message::success(t("Movies settings updated successfully")); + url::redirect("admin/movies"); + } + // Something went wrong - print view from existing form. + $this->_print_view($form); + } + + private function _print_view($form) { + list ($ffmpeg_version, $ffmpeg_date) = movie::get_ffmpeg_version(); + $ffmpeg_version = $ffmpeg_date ? "{$ffmpeg_version} ({$ffmpeg_date})" : $ffmpeg_version; + $ffmpeg_path = movie::find_ffmpeg(); + $ffmpeg_dir = substr($ffmpeg_path, 0, strrpos($ffmpeg_path, "/")); + + $view = new Admin_View("admin.html"); + $view->page_title = t("Movies settings"); + $view->content = new View("admin_movies.html"); + $view->content->form = $form; + $view->content->ffmpeg_dir = $ffmpeg_dir; + $view->content->ffmpeg_version = $ffmpeg_version; + print $view; + } + + private function _get_admin_form() { + $form = new Forge("admin/movies/save", "", "post", array("id" => "g-movies-admin-form")); + $group = $form->group("settings")->label(t("Settings")); + $group->dropdown("allow_uploads") + ->label(t("Allow movie uploads into Gallery (does not affect existing movies)")) + ->options(array("autodetect"=>t("only if FFmpeg is detected (default)"), + "always"=>t("always"), "never"=>t("never"))) + ->selected(module::get_var("gallery", "movie_allow_uploads", "autodetect")); + $group->checkbox("rebuild_thumbs") + ->label(t("Rebuild all movie thumbnails (once FFmpeg is installed, use this to update existing movie thumbnails)")) + ->checked(false); // always set as false + $form->submit("save")->value(t("Save")); + return $form; + } +} diff --git a/modules/gallery/controllers/admin_sidebar.php b/modules/gallery/controllers/admin_sidebar.php new file mode 100644 index 0000000..f150f85 --- /dev/null +++ b/modules/gallery/controllers/admin_sidebar.php @@ -0,0 +1,69 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Sidebar_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Manage sidebar"); + $view->content = new View("admin_sidebar.html"); + $view->content->csrf = access::csrf_token(); + $view->content->available = new View("admin_sidebar_blocks.html"); + $view->content->active = new View("admin_sidebar_blocks.html"); + list($view->content->available->blocks, $view->content->active->blocks) = $this->_get_blocks(); + print $view; + } + + public function update() { + access::verify_csrf(); + + $available_blocks = block_manager::get_available_site_blocks(); + + $active_blocks = array(); + foreach (Input::instance()->get("block", array()) as $block_id) { + $active_blocks[md5($block_id)] = explode(":", (string) $block_id); + } + block_manager::set_active("site_sidebar", $active_blocks); + + $result = array("result" => "success"); + list($available, $active) = $this->_get_blocks(); + $v = new View("admin_sidebar_blocks.html"); + $v->blocks = $available; + $result["available"] = $v->render(); + $v = new View("admin_sidebar_blocks.html"); + $v->blocks = $active; + $result["active"] = $v->render(); + $message = t("Updated sidebar blocks"); + $result["message"] = (string) $message; + json::reply($result); + } + + private function _get_blocks() { + $active_blocks = array(); + $available_blocks = block_manager::get_available_site_blocks(); + foreach (block_manager::get_active("site_sidebar") as $block) { + $id = "{$block[0]}:{$block[1]}"; + if (!empty($available_blocks[$id])) { + $active_blocks[$id] = $available_blocks[$id]; + unset($available_blocks[$id]); + } + } + return array($available_blocks, $active_blocks); + } +} + diff --git a/modules/gallery/controllers/admin_theme_options.php b/modules/gallery/controllers/admin_theme_options.php new file mode 100644 index 0000000..38d2b0a --- /dev/null +++ b/modules/gallery/controllers/admin_theme_options.php @@ -0,0 +1,120 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Theme_Options_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Theme options"); + $view->content = new View("admin_theme_options.html"); + $view->content->form = $this->_get_edit_form_admin(); + print $view; + } + + public function save() { + access::verify_csrf(); + + $form = $this->_get_edit_form_admin(); + if ($form->validate()) { + module::set_var("gallery", "page_size", $form->edit_theme->page_size->value); + + $thumb_size = $form->edit_theme->thumb_size->value; + if (module::get_var("gallery", "thumb_size") != $thumb_size) { + graphics::remove_rule("gallery", "thumb", "gallery_graphics::resize"); + graphics::add_rule( + "gallery", "thumb", "gallery_graphics::resize", + array("width" => $thumb_size, "height" => $thumb_size, "master" => Image::AUTO), + 100); + module::set_var("gallery", "thumb_size", $thumb_size); + } + + $resize_size = $form->edit_theme->resize_size->value; + if (module::get_var("gallery", "resize_size") != $resize_size) { + graphics::remove_rule("gallery", "resize", "gallery_graphics::resize"); + graphics::add_rule( + "gallery", "resize", "gallery_graphics::resize", + array("width" => $resize_size, "height" => $resize_size, "master" => Image::AUTO), + 100); + module::set_var("gallery", "resize_size", $resize_size); + } + + module::set_var("gallery", "header_text", $form->edit_theme->header_text->value); + module::set_var("gallery", "footer_text", $form->edit_theme->footer_text->value); + module::set_var("gallery", "show_credits", $form->edit_theme->show_credits->value); + module::set_var("gallery", "favicon_url", $form->edit_theme->favicon_url->value); + module::set_var("gallery", "apple_touch_icon_url", $form->edit_theme->apple_touch_icon_url->value); + + module::event("theme_edit_form_completed", $form); + + message::success(t("Updated theme details")); + url::redirect("admin/theme_options"); + } else { + $view = new Admin_View("admin.html"); + $view->content = new View("admin_theme_options.html"); + $view->content->form = $form; + print $view; + } + } + + private function _get_edit_form_admin() { + $form = new Forge("admin/theme_options/save/", "", null, array("id" =>"g-theme-options-form")); + $group = $form->group("edit_theme")->label(t("Theme layout")); + $group->input("page_size")->label(t("Items per page"))->id("g-page-size") + ->rules("required|valid_digit") + ->callback(array($this, "_valididate_page_size")) + ->error_messages("required", t("You must enter a number")) + ->error_messages("valid_digit", t("You must enter a number")) + ->error_messages("valid_min_value", t("The value must be greater than zero")) + ->value(module::get_var("gallery", "page_size")); + $group->input("thumb_size")->label(t("Thumbnail size (in pixels)"))->id("g-thumb-size") + ->rules("required|valid_digit") + ->error_messages("required", t("You must enter a number")) + ->error_messages("valid_digit", t("You must enter a number")) + ->value(module::get_var("gallery", "thumb_size")); + $group->input("resize_size")->label(t("Resized image size (in pixels)"))->id("g-resize-size") + ->rules("required|valid_digit") + ->error_messages("required", t("You must enter a number")) + ->error_messages("valid_digit", t("You must enter a number")) + ->value(module::get_var("gallery", "resize_size")); + $group->input("favicon_url")->label(t("URL (or relative path) to your favicon.ico")) + ->id("g-favicon") + ->value(module::get_var("gallery", "favicon_url")); + $group->input("apple_touch_icon_url")->label(t("URL (or relative path) to your Apple Touch icon")) + ->id("g-apple-touch") + ->value(module::get_var("gallery", "apple_touch_icon_url")); + $group->textarea("header_text")->label(t("Header text"))->id("g-header-text") + ->value(module::get_var("gallery", "header_text")); + $group->textarea("footer_text")->label(t("Footer text"))->id("g-footer-text") + ->value(module::get_var("gallery", "footer_text")); + $group->checkbox("show_credits")->label(t("Show site credits"))->id("g-footer-text") + ->checked(module::get_var("gallery", "show_credits")); + + module::event("theme_edit_form", $form); + + $group->submit("")->value(t("Save")); + return $form; + } + + function _valididate_page_size($input) { + if ($input->value < 1) { + $input->add_error("valid_min_value", true); + } + + } +} + diff --git a/modules/gallery/controllers/admin_themes.php b/modules/gallery/controllers/admin_themes.php new file mode 100644 index 0000000..4ab994f --- /dev/null +++ b/modules/gallery/controllers/admin_themes.php @@ -0,0 +1,80 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Themes_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Theme choice"); + $view->content = new View("admin_themes.html"); + $view->content->admin = module::get_var("gallery", "active_admin_theme"); + $view->content->site = module::get_var("gallery", "active_site_theme"); + $view->content->themes = $this->_get_themes(); + + site_status::clear("missing_site_theme"); + site_status::clear("missing_admin_theme"); + print $view; + } + + private function _get_themes() { + $themes = array(); + foreach (scandir(THEMEPATH) as $theme_name) { + if ($theme_name[0] == ".") { + continue; + } + $theme_name = preg_replace("/[^a-zA-Z0-9\._-]/", "", $theme_name); + if (file_exists(THEMEPATH . "$theme_name/theme.info")) { + + $themes[$theme_name] = theme::get_info($theme_name); + } + } + return $themes; + } + + public function preview($type, $theme_name) { + $view = new View("admin_themes_preview.html"); + $view->info = theme::get_info($theme_name); + $view->theme_name = t($theme_name); + $view->type = $type; + if ($type == "admin") { + $view->url = url::site("admin?theme=$theme_name"); + } else { + $view->url = item::root()->url("theme=$theme_name"); + } + print $view; + } + + public function choose($type, $theme_name) { + access::verify_csrf(); + + $info = theme::get_info($theme_name); + + if ($type == "admin" && $info->admin) { + module::set_var("gallery", "active_admin_theme", $theme_name); + message::success(t("Successfully changed your admin theme to <b>%theme_name</b>", + array("theme_name" => $info->name))); + } else if ($type == "site" && $info->site) { + module::set_var("gallery", "active_site_theme", $theme_name); + message::success(t("Successfully changed your Gallery theme to <b>%theme_name</b>", + array("theme_name" => $info->name))); + } + + url::redirect("admin/themes"); + } +} + diff --git a/modules/gallery/controllers/admin_upgrade_checker.php b/modules/gallery/controllers/admin_upgrade_checker.php new file mode 100644 index 0000000..3f0bdac --- /dev/null +++ b/modules/gallery/controllers/admin_upgrade_checker.php @@ -0,0 +1,57 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Upgrade_Checker_Controller extends Admin_Controller { + function check_now() { + access::verify_csrf(); + upgrade_checker::fetch_version_info(); + $message = upgrade_checker::get_upgrade_message(); + if ($message) { + $message .= t( + " <a href=\"%hide-url\"><i>(remind me later)</i></a>", + array("hide-url" => url::site("admin/upgrade_checker/remind_me_later?csrf=__CSRF__"))); + site_status::info($message, "upgrade_checker"); + } else { + site_status::clear("upgrade_checker"); + } + url::redirect("admin/dashboard"); + } + + function remind_me_later() { + access::verify_csrf(); + site_status::clear("upgrade_checker"); + if ($referer = Input::instance()->server("HTTP_REFERER")) { + url::redirect($referer); + } else { + url::redirect(item::root()->abs_url()); + } + } + + function set_auto($val) { + access::verify_csrf(); + module::set_var("gallery", "upgrade_checker_auto_enabled", (bool)$val); + + if ((bool)$val) { + message::success(t("Automatic upgrade checking is enabled.")); + } else { + message::success(t("Automatic upgrade checking is disabled.")); + } + url::redirect("admin/dashboard"); + } +} diff --git a/modules/gallery/controllers/albums.php b/modules/gallery/controllers/albums.php new file mode 100644 index 0000000..0fb033a --- /dev/null +++ b/modules/gallery/controllers/albums.php @@ -0,0 +1,207 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Albums_Controller extends Items_Controller { + public function index() { + $this->show(ORM::factory("item", 1)); + } + + public function show($album) { + if (!is_object($album)) { + // show() must be public because we route to it in url::parse_url(), so make + // sure that we're actually receiving an object + throw new Kohana_404_Exception(); + } + + access::required("view", $album); + + $page_size = module::get_var("gallery", "page_size", 9); + $input = Input::instance(); + $show = $input->get("show"); + + if ($show) { + $child = ORM::factory("item", $show); + $index = item::get_position($child); + if ($index) { + $page = ceil($index / $page_size); + if ($page == 1) { + url::redirect($album->abs_url()); + } else { + url::redirect($album->abs_url("page=$page")); + } + } + } + + $page = $input->get("page", "1"); + $children_count = $album->viewable()->children_count(); + $offset = ($page - 1) * $page_size; + $max_pages = max(ceil($children_count / $page_size), 1); + + // Make sure that the page references a valid offset + if ($page < 1) { + url::redirect($album->abs_url()); + } else if ($page > $max_pages) { + url::redirect($album->abs_url("page=$max_pages")); + } + + $template = new Theme_View("page.html", "collection", "album"); + $template->set_global( + array("page" => $page, + "page_title" => null, + "max_pages" => $max_pages, + "page_size" => $page_size, + "item" => $album, + "children" => $album->viewable()->children($page_size, $offset), + "parents" => $album->parents()->as_array(), // view calls empty() on this + "breadcrumbs" => Breadcrumb::array_from_item_parents($album), + "children_count" => $children_count)); + $template->content = new View("album.html"); + $album->increment_view_count(); + + print $template; + item::set_display_context_callback("Albums_Controller::get_display_context"); + } + + static function get_display_context($item) { + $where = array(array("type", "!=", "album")); + $position = item::get_position($item, $where); + if ($position > 1) { + list ($previous_item, $ignore, $next_item) = + $item->parent()->viewable()->children(3, $position - 2, $where); + } else { + $previous_item = null; + list ($next_item) = $item->parent()->viewable()->children(1, $position, $where); + } + + return array("position" => $position, + "previous_item" => $previous_item, + "next_item" => $next_item, + "sibling_count" => $item->parent()->viewable()->children_count($where), + "siblings_callback" => array("Albums_Controller::get_siblings", array($item)), + "parents" => $item->parents()->as_array(), + "breadcrumbs" => Breadcrumb::array_from_item_parents($item)); + } + + static function get_siblings($item, $limit=null, $offset=null) { + // @todo consider creating Item_Model::siblings() if we use this more broadly. + return $item->parent()->viewable()->children($limit, $offset); + } + + public function create($parent_id) { + access::verify_csrf(); + $album = ORM::factory("item", $parent_id); + access::required("view", $album); + access::required("add", $album); + + $form = album::get_add_form($album); + try { + $valid = $form->validate(); + $album = ORM::factory("item"); + $album->type = "album"; + $album->parent_id = $parent_id; + $album->name = $form->add_album->inputs["name"]->value; + $album->title = $form->add_album->title->value ? + $form->add_album->title->value : $form->add_album->inputs["name"]->value; + $album->description = $form->add_album->description->value; + $album->slug = $form->add_album->slug->value; + $album->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->add_album->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $album->save(); + module::event("album_add_form_completed", $album, $form); + log::success("content", "Created an album", + html::anchor("albums/$album->id", "view album")); + message::success(t("Created album %album_title", + array("album_title" => html::purify($album->title)))); + + json::reply(array("result" => "success", "location" => $album->url())); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function update($album_id) { + access::verify_csrf(); + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("edit", $album); + + $form = album::get_edit_form($album); + try { + $valid = $form->validate(); + $album->title = $form->edit_item->title->value; + $album->description = $form->edit_item->description->value; + $album->sort_column = $form->edit_item->sort_order->column->value; + $album->sort_order = $form->edit_item->sort_order->direction->value; + if (array_key_exists("name", $form->edit_item->inputs)) { + $album->name = $form->edit_item->inputs["name"]->value; + } + $album->slug = $form->edit_item->slug->value; + $album->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_item->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $album->save(); + module::event("item_edit_form_completed", $album, $form); + + log::success("content", "Updated album", "<a href=\"albums/$album->id\">view</a>"); + message::success(t("Saved album %album_title", + array("album_title" => html::purify($album->title)))); + + if ($form->from_id->value == $album->id) { + // Use the new url; it might have changed. + json::reply(array("result" => "success", "location" => $album->url())); + } else { + // Stay on the same page + json::reply(array("result" => "success")); + } + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_add($album_id) { + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("add", $album); + + print album::get_add_form($album); + } + + public function form_edit($album_id) { + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("edit", $album); + + print album::get_edit_form($album); + } +} diff --git a/modules/gallery/controllers/combined.php b/modules/gallery/controllers/combined.php new file mode 100644 index 0000000..3cf7f87 --- /dev/null +++ b/modules/gallery/controllers/combined.php @@ -0,0 +1,96 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Combined_Controller extends Controller { + const ALLOW_MAINTENANCE_MODE = true; + const ALLOW_PRIVATE_GALLERY = true; + + /** + * Return the combined Javascript bundle associated with the given key. + */ + public function javascript($key) { + return $this->_emit("javascript", $key); + } + + /** + * Return the combined CSS bundle associated with the given key. + */ + public function css($key) { + return $this->_emit("css", $key); + } + + /** + * Print out a cached entry. + * @param string the combined entry type (either "javascript" or "css") + * @param string the key (typically an md5 sum) + */ + private function _emit($type, $key) { + $input = Input::instance(); + + // We don't need to save the session for this request + Session::instance()->abort_save(); + + // Our data is immutable, so if they already have a copy then it needs no updating. + if ($input->server("HTTP_IF_MODIFIED_SINCE")) { + header('HTTP/1.0 304 Not Modified'); + header("Expires: Tue, 19 Jan 2038 00:00:00 GMT"); + header("Cache-Control: public,max-age=2678400"); + header('Pragma: public'); + Kohana::close_buffers(false); + return ""; + } + + if (empty($key)) { + throw new Kohana_404_Exception(); + } + + $cache = Cache::instance(); + $use_gzip = function_exists("gzencode") && + stripos($input->server("HTTP_ACCEPT_ENCODING"), "gzip") !== false && + (int) ini_get("zlib.output_compression") === 0; + + if ($use_gzip && $content = $cache->get("{$key}_gz")) { + header("Content-Encoding: gzip"); + header("Vary: Accept-Encoding"); + } else { + // Fall back to non-gzipped if we have to + $content = $cache->get($key); + } + if (empty($content)) { + throw new Kohana_404_Exception(); + } + + // $type is either 'javascript' or 'css' + if ($type == "javascript") { + header("Content-Type: application/javascript; charset=UTF-8"); + } else { + header("Content-Type: text/css; charset=UTF-8"); + } + header("Expires: Tue, 19 Jan 2038 00:00:00 GMT"); + header("Cache-Control: public,max-age=2678400"); + header("Pragma: public"); + header("Last-Modified: " . gmdate("D, d M Y H:i:s T", time())); + header("Content-Length: " . strlen($content)); + + Kohana::close_buffers(false); + print $content; + } + +} + diff --git a/modules/gallery/controllers/file_proxy.php b/modules/gallery/controllers/file_proxy.php new file mode 100644 index 0000000..34f6b8c --- /dev/null +++ b/modules/gallery/controllers/file_proxy.php @@ -0,0 +1,174 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/** + * Proxy access to files in var/albums and var/resizes, making sure that the session user has + * access to view these files. + * + * Security Philosophy: we do not use the information provided to find if the file exists on + * disk. We use this information only to locate the correct item in the database and then we + * *only* use information from the database to find and proxy the correct file. This way all user + * input is sanitized against the database before we perform any file I/O. + */ +class File_Proxy_Controller extends Controller { + const ALLOW_PRIVATE_GALLERY = true; + public function __call($function, $args) { + + // Force zlib compression off. Image and movie files are already compressed and + // recompressing them is CPU intensive. + if (ini_get("zlib.output_compression")) { + ini_set("zlib.output_compression", "Off"); + } + + // request_uri: gallery3/var/albums/foo/bar.jpg?m=1234 + $request_uri = rawurldecode(Input::instance()->server("REQUEST_URI")); + + // get rid of query parameters + // request_uri: gallery3/var/albums/foo/bar.jpg + $request_uri = preg_replace("/\?.*/", "", $request_uri); + + // var_uri: gallery3/var/ + $var_uri = url::file("var/"); + + // Make sure that the request is for a file inside var + $offset = strpos(rawurldecode($request_uri), $var_uri); + if ($offset !== 0) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 1; + throw $e; + } + + // file_uri: albums/foo/bar.jpg + $file_uri = substr($request_uri, strlen($var_uri)); + + // type: albums + // path: foo/bar.jpg + list ($type, $path) = explode("/", $file_uri, 2); + if ($type != "resizes" && $type != "albums" && $type != "thumbs") { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 2; + throw $e; + } + + // If the last element is .album.jpg, pop that off since it's not a real item + $path = preg_replace("|/.album.jpg$|", "", $path); + + $item = item::find_by_path($path); + if (!$item->loaded()) { + // We didn't turn it up. If we're looking for a .jpg then it's it's possible that we're + // requesting the thumbnail for a movie. In that case, the movie file would + // have been converted to a .jpg. So try some alternate types: + if (preg_match('/.jpg$/', $path)) { + foreach (legal_file::get_movie_extensions() as $ext) { + $movie_path = preg_replace('/.jpg$/', ".$ext", $path); + $item = item::find_by_path($movie_path); + if ($item->loaded()) { + break; + } + } + } + } + + if (!$item->loaded()) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 3; + throw $e; + } + + // Make sure we have access to the item + if (!access::can("view", $item)) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 4; + throw $e; + } + + // Make sure we have view_full access to the original + if ($type == "albums" && !access::can("view_full", $item)) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 5; + throw $e; + } + + // Don't try to load a directory + if ($type == "albums" && $item->is_album()) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 6; + throw $e; + } + + // Note: this code is roughly duplicated in data_rest, so if you modify this, please look to + // see if you should make the same change there as well. + + if ($type == "albums") { + $file = $item->file_path(); + } else if ($type == "resizes") { + $file = $item->resize_path(); + } else { + $file = $item->thumb_path(); + } + + if (!file_exists($file)) { + $e = new Kohana_404_Exception(); + $e->test_fail_code = 7; + throw $e; + } + + if (gallery::show_profiler()) { + Profiler::enable(); + $profiler = new Profiler(); + $profiler->render(); + exit; + } + + header("Content-Length: " . filesize($file)); + + header("Pragma:"); + // Check that the content hasn't expired or it wasn't changed since cached + expires::check(2592000, $item->updated); + + // We don't need to save the session for this request + Session::instance()->abort_save(); + + expires::set(2592000, $item->updated); // 30 days + + // Dump out the image. If the item is a movie or album, then its thumbnail will be a JPG. + if (($item->is_movie() || $item->is_album()) && $type == "thumbs") { + header("Content-Type: image/jpeg"); + } else { + header("Content-Type: $item->mime_type"); + } + + if (TEST_MODE) { + return $file; + } else { + // Don't use Kohana::close_buffers(false) here because that only closes all the buffers + // that Kohana started. We want to close *all* buffers at this point because otherwise we're + // going to buffer up whatever file we're proxying (and it may be very large). This may + // affect embedding or systems with PHP's output_buffering enabled. + while (ob_get_level()) { + if (!@ob_end_clean()) { + // ob_end_clean() can return false if the buffer can't be removed for some reason + // (zlib output compression buffers sometimes cause problems). + break; + } + } + readfile($file); + } + } +} diff --git a/modules/gallery/controllers/items.php b/modules/gallery/controllers/items.php new file mode 100644 index 0000000..845df76 --- /dev/null +++ b/modules/gallery/controllers/items.php @@ -0,0 +1,43 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Items_Controller extends Controller { + public function __call($function, $args) { + $item = ORM::factory("item", (int)$function); + if (!$item->loaded()) { + throw new Kohana_404_Exception(); + } + + // Redirect to the more specific resource type, since it will render differently. We can't + // delegate here because we may have gotten to this page via /items/<id> which means that we + // don't have a type-specific controller. Also, we want to drive a single canonical resource + // mapping where possible. + access::required("view", $item); + url::redirect($item->abs_url()); + } + + // Return the width/height dimensions for the given item + public function dimensions($id) { + $item = ORM::factory("item", $id); + access::required("view", $item); + json::reply(array("thumb" => array((int)$item->thumb_width, (int)$item->thumb_height), + "resize" => array((int)$item->resize_width, (int)$item->resize_height), + "full" => array((int)$item->width, (int)$item->height))); + } +} diff --git a/modules/gallery/controllers/l10n_client.php b/modules/gallery/controllers/l10n_client.php new file mode 100644 index 0000000..993dfb3 --- /dev/null +++ b/modules/gallery/controllers/l10n_client.php @@ -0,0 +1,179 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class L10n_Client_Controller extends Controller { + public function save() { + access::verify_csrf(); + if (!identity::active_user()->admin) { + access::forbidden(); + } + + $locale = Gallery_I18n::instance()->locale(); + $input = Input::instance(); + $key = $input->post("l10n-message-key"); + + $root_message = ORM::factory("incoming_translation") + ->where("key", "=", $key) + ->where("locale", "=", "root") + ->find(); + + if (!$root_message->loaded()) { + throw new Exception("@todo bad request data / illegal state"); + } + $is_plural = Gallery_I18n::is_plural_message(unserialize($root_message->message)); + + $is_empty = true; + if ($is_plural) { + $plural_forms = l10n_client::plural_forms($locale); + $translation = array(); + foreach($plural_forms as $plural_form) { + $value = $input->post("l10n-edit-plural-translation-$plural_form"); + if (null === $value || !is_string($value)) { + throw new Exception("@todo bad request data"); + } + $translation[$plural_form] = $value; + $is_empty = $is_empty && empty($value); + } + } else { + $translation = $input->post("l10n-edit-translation"); + $is_empty = empty($translation); + if (null === $translation || !is_string($translation)) { + throw new Exception("@todo bad request data"); + } + } + + $entry = ORM::factory("outgoing_translation") + ->where("key", "=", $key) + ->where("locale", "=", $locale) + ->find(); + + if ($is_empty) { + if ($entry->loaded()) { + $entry->delete(); + } + } else { + if (!$entry->loaded()) { + $entry->key = $key; + $entry->locale = $locale; + $entry->message = $root_message->message; + $entry->base_revision = null; + } + + $entry->translation = serialize($translation); + + $entry_from_incoming = ORM::factory("incoming_translation") + ->where("key", "=", $key) + ->where("locale", "=", $locale) + ->find(); + + if (!$entry_from_incoming->loaded()) { + $entry->base_revision = $entry_from_incoming->revision; + } + + $entry->save(); + } + + Gallery_I18n::clear_cache($locale); + + json::reply(new stdClass()); + } + + public function toggle_l10n_mode() { + access::verify_csrf(); + if (!identity::active_user()->admin) { + access::forbidden(); + } + + $session = Session::instance(); + $l10n_mode = $session->get("l10n_mode", false); + $session->set("l10n_mode", !$l10n_mode); + + $redirect_url = "admin/languages"; + if (!$l10n_mode) { + $redirect_url .= "#l10n-client"; + } + + url::redirect($redirect_url); + } + + private static function _l10n_client_search_form() { + $form = new Forge("#", "", "post", array("id" => "g-l10n-search-form")); + $group = $form->group("l10n_search"); + $group->input("l10n-search")->id("g-l10n-search"); + + return $form; + } + + public static function l10n_form() { + if (Input::instance()->get("show_all_l10n_messages")) { + $calls = array(); + foreach (db::build() + ->select("key", "message") + ->from("incoming_translations") + ->where("locale", "=", "root") + ->execute() as $row) { + $calls[$row->key] = array(unserialize($row->message), array()); + } + } else { + $calls = Gallery_I18n::instance()->call_log(); + } + $locale = Gallery_I18n::instance()->locale(); + + if ($calls) { + $translations = array(); + foreach (db::build() + ->select("key", "translation") + ->from("incoming_translations") + ->where("locale", "=", $locale) + ->execute() as $row) { + $translations[$row->key] = unserialize($row->translation); + } + // Override incoming with outgoing... + foreach (db::build() + ->select("key", "translation") + ->from("outgoing_translations") + ->where("locale", "=", $locale) + ->execute() as $row) { + $translations[$row->key] = unserialize($row->translation); + } + + $string_list = array(); + $cache = array(); + foreach ($calls as $key => $call) { + list ($message, $options) = $call; + // Ensure that the message is in the DB + l10n_scanner::process_message($message, $cache); + // Note: Not interpolating placeholders for the actual translation input field. + // TODO: Might show a preview w/ interpolations (using $options) + $translation = isset($translations[$key]) ? $translations[$key] : ''; + $string_list[] = array('source' => $message, + 'key' => $key, + 'translation' => $translation); + } + + $v = new View('l10n_client.html'); + $v->string_list = $string_list; + $v->l10n_search_form = self::_l10n_client_search_form(); + $v->plural_forms = l10n_client::plural_forms($locale); + return $v; + } + + return ''; + } +} diff --git a/modules/gallery/controllers/login.php b/modules/gallery/controllers/login.php new file mode 100644 index 0000000..9da575b --- /dev/null +++ b/modules/gallery/controllers/login.php @@ -0,0 +1,90 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Login_Controller extends Controller { + const ALLOW_MAINTENANCE_MODE = true; + const ALLOW_PRIVATE_GALLERY = true; + + public function ajax() { + $view = new View("login_ajax.html"); + $view->form = auth::get_login_form("login/auth_ajax"); + print $view; + } + + public function auth_ajax() { + access::verify_csrf(); + + list ($valid, $form) = $this->_auth("login/auth_ajax"); + if ($valid) { + json::reply(array("result" => "success")); + } else { + $view = new View("login_ajax.html"); + $view->form = $form; + json::reply(array("result" => "error", "html" => (string)$view)); + } + } + + public function html() { + $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; + } + + public function auth_html() { + access::verify_csrf(); + + list ($valid, $form) = $this->_auth("login/auth_html"); + if ($valid) { + $continue_url = $form->continue_url->value; + 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"); + $view->content = new View("login_ajax.html"); + $view->content->form = $form; + print $view; + } + } + + private function _auth($url) { + $form = auth::get_login_form($url); + $valid = $form->validate(); + if ($valid) { + $user = identity::lookup_user_by_name($form->login->inputs["name"]->value); + if (empty($user) || !identity::is_correct_password($user, $form->login->password->value)) { + $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_auth_failed", $name); + $valid = false; + } + } + + if ($valid) { + auth::login($user); + } + + // Either way, regenerate the session id to avoid session trapping + Session::instance()->regenerate(); + + return array($valid, $form); + } +}
\ No newline at end of file diff --git a/modules/gallery/controllers/logout.php b/modules/gallery/controllers/logout.php new file mode 100644 index 0000000..9a24d1b --- /dev/null +++ b/modules/gallery/controllers/logout.php @@ -0,0 +1,29 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Logout_Controller extends Controller { + public function index() { + access::verify_csrf(); + auth::logout(); + if ($continue_url = Input::instance()->get("continue_url")) { + url::redirect($continue_url); + } + url::redirect(item::root()->abs_url()); + } +}
\ No newline at end of file diff --git a/modules/gallery/controllers/movies.php b/modules/gallery/controllers/movies.php new file mode 100644 index 0000000..5607571 --- /dev/null +++ b/modules/gallery/controllers/movies.php @@ -0,0 +1,91 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Movies_Controller extends Items_Controller { + public function show($movie) { + if (!is_object($movie)) { + // show() must be public because we route to it in url::parse_url(), so make + // sure that we're actually receiving an object + throw new Kohana_404_Exception(); + } + + access::required("view", $movie); + + $template = new Theme_View("page.html", "item", "movie"); + $template->set_global(array("item" => $movie, + "children" => array(), + "children_count" => 0)); + $template->set_global(item::get_display_context($movie)); + $template->content = new View("movie.html"); + + $movie->increment_view_count(); + + print $template; + } + + public function update($movie_id) { + access::verify_csrf(); + $movie = ORM::factory("item", $movie_id); + access::required("view", $movie); + access::required("edit", $movie); + + $form = movie::get_edit_form($movie); + try { + $valid = $form->validate(); + $movie->title = $form->edit_item->title->value; + $movie->description = $form->edit_item->description->value; + $movie->slug = $form->edit_item->slug->value; + $movie->name = $form->edit_item->inputs["name"]->value; + $movie->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_item->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $movie->save(); + module::event("item_edit_form_completed", $movie, $form); + + log::success("content", "Updated movie", "<a href=\"{$movie->url()}\">view</a>"); + message::success( + t("Saved movie %movie_title", array("movie_title" => html::purify($movie->title)))); + + if ($form->from_id->value == $movie->id) { + // Use the new url; it might have changed. + json::reply(array("result" => "success", "location" => $movie->url())); + } else { + // Stay on the same page + json::reply(array("result" => "success")); + } + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + } + + public function form_edit($movie_id) { + $movie = ORM::factory("item", $movie_id); + access::required("view", $movie); + access::required("edit", $movie); + + print movie::get_edit_form($movie); + } +} diff --git a/modules/gallery/controllers/packager.php b/modules/gallery/controllers/packager.php new file mode 100644 index 0000000..d7e3cf4 --- /dev/null +++ b/modules/gallery/controllers/packager.php @@ -0,0 +1,191 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Packager_Controller extends Controller { + function package() { + if (PHP_SAPI != "cli") { + access::forbidden(); + } + + $_SERVER["HTTP_HOST"] = "example.com"; + + try { + $this->_reset(); // empty and reinstall the standard modules + $this->_dump_database(); // Dump the database + $this->_dump_var(); // Dump the var directory + } catch (Exception $e) { + print $e->getMessage() . "\n" . $e->getTraceAsString(); + return; + } + + print "Successfully wrote install.sql and init_var.php\n"; + } + + private function _reset() { + // Drop all tables + foreach (Database::instance()->list_tables() as $table) { + Database::instance()->query("DROP TABLE IF EXISTS {{$table}}"); + } + + // Clean out data + dir::unlink(VARPATH . "uploads"); + dir::unlink(VARPATH . "albums"); + dir::unlink(VARPATH . "resizes"); + dir::unlink(VARPATH . "thumbs"); + dir::unlink(VARPATH . "modules"); + dir::unlink(VARPATH . "tmp"); + + Database::instance()->clear_cache(); + module::$modules = array(); + module::$active = array(); + + // Use a known random seed so that subsequent packaging runs will reuse the same random + // numbers, keeping our install.sql file more stable. + srand(0); + + foreach (array("gallery", "user", "comment", "organize", "info", + "rss", "search", "slideshow", "tag") as $module_name) { + module::install($module_name); + module::activate($module_name); + } + } + + private function _dump_database() { + // We now have a clean install with just the packages that we want. Make sure that the + // database is clean too. + $i = 1; + foreach (array("dashboard_sidebar", "dashboard_center", "site_sidebar") as $key) { + $blocks = array(); + foreach (unserialize(module::get_var("gallery", "blocks_{$key}")) as $rnd => $value) { + $blocks[++$i] = $value; + } + module::set_var("gallery", "blocks_{$key}", serialize($blocks)); + } + + Database::instance()->query("TRUNCATE {caches}"); + Database::instance()->query("TRUNCATE {sessions}"); + Database::instance()->query("TRUNCATE {logs}"); + db::build()->update("users") + ->set(array("password" => "")) + ->where("id", "in", array(1, 2)) + ->execute(); + + $dbconfig = Kohana::config('database.default'); + $conn = $dbconfig["connection"]; + $sql_file = DOCROOT . "installer/install.sql"; + if (!is_writable($sql_file)) { + print "$sql_file is not writeable"; + return; + } + $command = sprintf( + "mysqldump --compact --skip-extended-insert --add-drop-table %s %s %s %s > $sql_file", + escapeshellarg("-h{$conn['host']}"), + escapeshellarg("-u{$conn['user']}"), + $conn['pass'] ? escapeshellarg("-p{$conn['pass']}") : "", + escapeshellarg($conn['database'])); + exec($command, $output, $status); + if ($status) { + print "<pre>"; + print "$command\n"; + print "Failed to dump database\n"; + print implode("\n", $output); + return; + } + + // Post-process the sql file + $buf = ""; + $root = ORM::factory("item", 1); + $root_created_timestamp = $root->created; + $root_updated_timestamp = $root->updated; + $table_name = ""; + foreach (file($sql_file) as $line) { + // Prefix tables + $line = preg_replace( + "/(CREATE TABLE|IF EXISTS|INSERT INTO) `{$dbconfig['table_prefix']}(\w+)`/", "\\1 {\\2}", + $line); + + if (preg_match("/CREATE TABLE {(\w+)}/", $line, $matches)) { + $table_name = $matches[1]; + } + // Normalize dates + $line = preg_replace("/,$root_created_timestamp,/", ",UNIX_TIMESTAMP(),", $line); + $line = preg_replace("/,$root_updated_timestamp,/", ",UNIX_TIMESTAMP(),", $line); + + // Remove ENGINE= specifications execpt for search records, it always needs to be MyISAM + if ($table_name != "search_records") { + $line = preg_replace("/ENGINE=\S+ /", "", $line); + } + + // Null out ids in the vars table since it's an auto_increment table and this will result in + // more stable values so we'll have less churn in install.sql. + $line = preg_replace( + "/^INSERT INTO {vars} VALUES \(\d+/", "INSERT INTO {vars} VALUES (NULL", $line); + + $buf .= $line; + } + $fd = fopen($sql_file, "wb"); + fwrite($fd, $buf); + fclose($fd); + } + + private function _dump_var() { + $objects = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(VARPATH), + RecursiveIteratorIterator::SELF_FIRST); + + $var_file = DOCROOT . "installer/init_var.php"; + if (!is_writable($var_file)) { + print "$var_file is not writeable"; + return; + } + + $paths = array(); + foreach($objects as $name => $file){ + $path = $file->getPath(); + $basename = $file->getBasename(); + if ($basename == "database.php" || $basename == "." || $basename == "..") { + continue; + } else if (basename($path) == "logs" && $basename != ".htaccess") { + continue; + } + + if ($file->isDir()) { + $paths[] = "VARPATH . \"" . substr($name, strlen(VARPATH)) . "\""; + } else { + // @todo: serialize non-directories + $files["VARPATH . \"" . substr($name, strlen(VARPATH)) . "\""] = + base64_encode(file_get_contents($name)); + } + } + // Sort the paths so that the var file is stable + sort($paths); + + $fd = fopen($var_file, "w"); + fwrite($fd, "<?php defined(\"SYSPATH\") or die(\"No direct script access.\") ?>\n"); + fwrite($fd, "<?php\n"); + foreach ($paths as $path) { + fwrite($fd, "!file_exists($path) && mkdir($path);\n"); + } + ksort($files); + foreach ($files as $file => $contents) { + fwrite($fd, "file_put_contents($file, base64_decode(\"$contents\"));\n"); + } + fclose($fd); + } +}
\ No newline at end of file diff --git a/modules/gallery/controllers/permissions.php b/modules/gallery/controllers/permissions.php new file mode 100644 index 0000000..2513f86 --- /dev/null +++ b/modules/gallery/controllers/permissions.php @@ -0,0 +1,91 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Permissions_Controller extends Controller { + function browse($id) { + $item = ORM::factory("item", $id); + access::required("view", $item); + access::required("edit", $item); + + if (!$item->is_album()) { + access::forbidden(); + } + + $view = new View("permissions_browse.html"); + $view->htaccess_works = access::htaccess_works(); + $view->item = $item; + $view->parents = $item->parents(); + $view->form = $this->_get_form($item); + + print $view; + } + + function form($id) { + $item = ORM::factory("item", $id); + access::required("view", $item); + access::required("edit", $item); + + if (!$item->is_album()) { + access::forbidden(); + } + + print $this->_get_form($item); + } + + function change($command, $group_id, $perm_id, $item_id) { + access::verify_csrf(); + + $group = identity::lookup_group($group_id); + $perm = ORM::factory("permission", $perm_id); + $item = ORM::factory("item", $item_id); + access::required("view", $item); + access::required("edit", $item); + + if (!empty($group) && $perm->loaded() && $item->loaded()) { + switch($command) { + case "allow": + access::allow($group, $perm->name, $item); + break; + + case "deny": + access::deny($group, $perm->name, $item); + break; + + case "reset": + access::reset($group, $perm->name, $item); + break; + } + + // If the active user just took away their own edit permissions, give it back. + if ($perm->name == "edit") { + if (!access::user_can(identity::active_user(), "edit", $item)) { + access::allow($group, $perm->name, $item); + } + } + } + } + + private function _get_form($item) { + $view = new View("permissions_form.html"); + $view->item = $item; + $view->groups = identity::groups(); + $view->permissions = ORM::factory("permission")->find_all(); + return $view; + } +} diff --git a/modules/gallery/controllers/photos.php b/modules/gallery/controllers/photos.php new file mode 100644 index 0000000..96a22c5 --- /dev/null +++ b/modules/gallery/controllers/photos.php @@ -0,0 +1,91 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Photos_Controller extends Items_Controller { + public function show($photo) { + if (!is_object($photo)) { + // show() must be public because we route to it in url::parse_url(), so make + // sure that we're actually receiving an object + throw new Kohana_404_Exception(); + } + + access::required("view", $photo); + + $template = new Theme_View("page.html", "item", "photo"); + $template->set_global(array("item" => $photo, + "children" => array(), + "children_count" => 0)); + $template->set_global(item::get_display_context($photo)); + $template->content = new View("photo.html"); + + $photo->increment_view_count(); + + print $template; + } + + public function update($photo_id) { + access::verify_csrf(); + $photo = ORM::factory("item", $photo_id); + access::required("view", $photo); + access::required("edit", $photo); + + $form = photo::get_edit_form($photo); + try { + $valid = $form->validate(); + $photo->title = $form->edit_item->title->value; + $photo->description = $form->edit_item->description->value; + $photo->slug = $form->edit_item->slug->value; + $photo->name = $form->edit_item->inputs["name"]->value; + $photo->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_item->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $photo->save(); + module::event("item_edit_form_completed", $photo, $form); + + log::success("content", "Updated photo", "<a href=\"{$photo->url()}\">view</a>"); + message::success( + t("Saved photo %photo_title", array("photo_title" => html::purify($photo->title)))); + + if ($form->from_id->value == $photo->id) { + // Use the new url; it might have changed. + json::reply(array("result" => "success", "location" => $photo->url())); + } else { + // Stay on the same page + json::reply(array("result" => "success")); + } + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_edit($photo_id) { + $photo = ORM::factory("item", $photo_id); + access::required("view", $photo); + access::required("edit", $photo); + + print photo::get_edit_form($photo); + } +} diff --git a/modules/gallery/controllers/quick.php b/modules/gallery/controllers/quick.php new file mode 100644 index 0000000..4b21d9e --- /dev/null +++ b/modules/gallery/controllers/quick.php @@ -0,0 +1,144 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Quick_Controller extends Controller { + public function rotate($id, $dir) { + access::verify_csrf(); + $item = model_cache::get("item", $id); + access::required("view", $item); + access::required("edit", $item); + + $degrees = 0; + switch($dir) { + case "ccw": + $degrees = -90; + break; + + case "cw": + $degrees = 90; + break; + } + + if ($degrees) { + $tmpfile = system::temp_filename("rotate", + pathinfo($item->file_path(), PATHINFO_EXTENSION)); + gallery_graphics::rotate($item->file_path(), $tmpfile, array("degrees" => $degrees), $item); + $item->set_data_file($tmpfile); + $item->save(); + } + + if (Input::instance()->get("page_type") == "collection") { + json::reply( + array("src" => $item->thumb_url(), + "width" => $item->thumb_width, + "height" => $item->thumb_height)); + } else { + json::reply( + array("src" => $item->resize_url(), + "width" => $item->resize_width, + "height" => $item->resize_height)); + } + } + + public function make_album_cover($id) { + access::verify_csrf(); + + $item = model_cache::get("item", $id); + access::required("view", $item); + access::required("view", $item->parent()); + access::required("edit", $item->parent()); + + $msg = t("Made <b>%title</b> this album's cover", array("title" => html::purify($item->title))); + + item::make_album_cover($item); + message::success($msg); + + json::reply(array("result" => "success", "reload" => 1)); + } + + public function form_delete($id) { + $item = model_cache::get("item", $id); + access::required("view", $item); + access::required("edit", $item); + + $v = new View("quick_delete_confirm.html"); + $v->item = $item; + $v->form = item::get_delete_form($item); + print $v; + } + + public function delete($id) { + access::verify_csrf(); + $item = model_cache::get("item", $id); + access::required("view", $item); + access::required("edit", $item); + + if ($item->is_album()) { + $msg = t("Deleted album <b>%title</b>", array("title" => html::purify($item->title))); + } else { + $msg = t("Deleted photo <b>%title</b>", array("title" => html::purify($item->title))); + } + + $parent = $item->parent(); + + if ($item->is_album()) { + // Album delete will trigger deletes for all children. Do this in a batch so that we can be + // smart about notifications, album cover updates, etc. + batch::start(); + $item->delete(); + batch::stop(); + } else { + $item->delete(); + } + message::success($msg); + + $from_id = Input::instance()->get("from_id"); + if (Input::instance()->get("page_type") == "collection" && + $from_id != $id /* deleted the item we were viewing */) { + json::reply(array("result" => "success", "reload" => 1)); + } else { + json::reply(array("result" => "success", "location" => $parent->url())); + } + } + + public function form_edit($id) { + $item = model_cache::get("item", $id); + access::required("view", $item); + access::required("edit", $item); + + switch ($item->type) { + case "album": + $form = album::get_edit_form($item); + break; + + case "photo": + $form = photo::get_edit_form($item); + break; + + case "movie": + $form = movie::get_edit_form($item); + break; + } + + // Pass on the source item where this form was generated, so we have an idea where to return to. + $form->hidden("from_id")->value((int)Input::instance()->get("from_id", 0)); + + print $form; + } +} diff --git a/modules/gallery/controllers/reauthenticate.php b/modules/gallery/controllers/reauthenticate.php new file mode 100644 index 0000000..c79b76e --- /dev/null +++ b/modules/gallery/controllers/reauthenticate.php @@ -0,0 +1,105 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Reauthenticate_Controller extends Controller { + public function index() { + $is_ajax = Session::instance()->get_once("is_ajax_request", request::is_ajax()); + if (!identity::active_user()->admin) { + if ($is_ajax) { + // We should never be able to get here since Admin_Controller::_reauth_check() won't work + // for non-admins. + access::forbidden(); + } else { + url::redirect(item::root()->abs_url()); + } + } + + // On redirects from the admin controller, the ajax request indicator is lost, + // so we store it in the session. + if ($is_ajax) { + $v = new View("reauthenticate.html"); + $v->form = self::_form(); + $v->user_name = identity::active_user()->name; + print $v; + } else { + 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) { + module::event("user_auth", $user); + if (!request::is_ajax()) { + message::success(t("Successfully re-authenticated!")); + } + url::redirect(Session::instance()->get_once("continue_url")); + } else { + $name = $user->name; + log::warning("user", t("Failed re-authentication for %name", array("name" => $name))); + module::event("user_auth_failed", $name); + if (request::is_ajax()) { + $v = new View("reauthenticate.html"); + $v->form = $form; + $v->user_name = identity::active_user()->name; + json::reply(array("html" => (string)$v)); + } else { + 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("Reauthenticate_Controller::valid_password") + ->error_messages("invalid_password", 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; + } + + static function valid_password($password_input) { + if (!identity::is_correct_password(identity::active_user(), $password_input->value)) { + $password_input->add_error("invalid_password", 1); + } + } +} diff --git a/modules/gallery/controllers/upgrader.php b/modules/gallery/controllers/upgrader.php new file mode 100644 index 0000000..6b3a9ef --- /dev/null +++ b/modules/gallery/controllers/upgrader.php @@ -0,0 +1,118 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Upgrader_Controller extends Controller { + public function index() { + $session = Session::instance(); + + // Make sure we have an upgrade token + if (!($upgrade_token = $session->get("upgrade_token", null))) { + $session->set("upgrade_token", $upgrade_token = random::hash()); + } + + // If the upgrade token exists, then bless this session + if (file_exists(TMPPATH . $upgrade_token)) { + $session->set("can_upgrade", true); + @unlink(TMPPATH . $upgrade_token); + } + + $available_upgrades = 0; + foreach (module::available() as $module) { + if ($module->version && $module->version != $module->code_version) { + $available_upgrades++; + } + } + + $failed = Input::instance()->get("failed"); + $view = new View("upgrader.html"); + $view->can_upgrade = identity::active_user()->admin || $session->get("can_upgrade"); + $view->upgrade_token = $upgrade_token; + $view->available = module::available(); + $view->failed = $failed ? explode(",", $failed) : array(); + $view->done = $available_upgrades == 0; + $view->obsolete_modules_message = module::get_obsolete_modules_message(); + print $view; + } + + public function upgrade() { + if (php_sapi_name() == "cli") { + // @todo this may screw up some module installers, but we don't have a better answer at + // this time. + $_SERVER["HTTP_HOST"] = "example.com"; + } else { + if (!identity::active_user()->admin && !Session::instance()->get("can_upgrade", false)) { + access::forbidden(); + } + + try { + access::verify_csrf(); + } catch (Exception $e) { + url::redirect("upgrader"); + } + } + + $available = module::available(); + // Upgrade gallery first + $gallery = $available["gallery"]; + if ($gallery->code_version != $gallery->version) { + module::upgrade("gallery"); + module::activate("gallery"); + } + + // Then upgrade the rest + $failed = array(); + foreach (module::available() as $id => $module) { + if ($id == "gallery") { + continue; + } + + if ($module->active && $module->code_version != $module->version) { + try { + module::upgrade($id); + } catch (Exception $e) { + // @todo assume it's MODULE_FAILED_TO_UPGRADE for now + $failed[] = $id; + } + } + } + + // If the upgrade failed, this will get recreated + site_status::clear("upgrade_now"); + + // Clear any upgrade check strings, we are probably up to date. + site_status::clear("upgrade_checker"); + + if (php_sapi_name() == "cli") { + if ($failed) { + print "Upgrade completed ** WITH FAILURES **\n"; + print "The following modules were not successfully upgraded:\n"; + print " " . implode($failed, "\n ") . "\n"; + print "Try getting newer versions or deactivating those modules\n"; + } else { + print "Upgrade complete\n"; + } + } else { + if ($failed) { + url::redirect("upgrader?failed=" . join(",", $failed)); + } else { + url::redirect("upgrader"); + } + } + } +} diff --git a/modules/gallery/controllers/uploader.php b/modules/gallery/controllers/uploader.php new file mode 100644 index 0000000..8e09dbe --- /dev/null +++ b/modules/gallery/controllers/uploader.php @@ -0,0 +1,133 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Uploader_Controller extends Controller { + public function index($id) { + $item = ORM::factory("item", $id); + access::required("view", $item); + access::required("add", $item); + if (!$item->is_album()) { + $item = $item->parent(); + } + + print $this->_get_add_form($item); + } + + public function start() { + access::verify_csrf(); + batch::start(); + } + + public function add_photo($id) { + $album = ORM::factory("item", $id); + access::required("view", $album); + access::required("add", $album); + access::verify_csrf(); + + // The Flash uploader not call /start directly, so simulate it here for now. + if (!batch::in_progress()) { + batch::start(); + } + + $form = $this->_get_add_form($album); + + // Uploadify adds its own field to the form, so validate that separately. + $file_validation = new Validation($_FILES); + $file_validation->add_rules( + "Filedata", "upload::valid", "upload::required", + "upload::type[" . implode(",", legal_file::get_extensions()) . "]"); + + if ($form->validate() && $file_validation->validate()) { + $temp_filename = upload::save("Filedata"); + system::delete_later($temp_filename); + try { + $item = ORM::factory("item"); + $item->name = substr(basename($temp_filename), 10); // Skip unique identifier Kohana adds + $item->title = item::convert_filename_to_title($item->name); + $item->parent_id = $album->id; + $item->set_data_file($temp_filename); + + $path_info = @pathinfo($temp_filename); + if (array_key_exists("extension", $path_info) && + legal_file::get_movie_extensions($path_info["extension"])) { + $item->type = "movie"; + $item->save(); + log::success("content", t("Added a movie"), + html::anchor("movies/$item->id", t("view movie"))); + } else { + $item->type = "photo"; + $item->save(); + log::success("content", t("Added a photo"), + html::anchor("photos/$item->id", t("view photo"))); + } + + module::event("add_photos_form_completed", $item, $form); + } catch (Exception $e) { + // The Flash uploader has no good way of reporting complex errors, so just keep it simple. + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + + // Ugh. I hate to use instanceof, But this beats catching the exception separately since + // we mostly want to treat it the same way as all other exceptions + if ($e instanceof ORM_Validation_Exception) { + Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1)); + } + + header("HTTP/1.1 500 Internal Server Error"); + print "ERROR: " . $e->getMessage(); + return; + } + print "FILEID: $item->id"; + } else { + header("HTTP/1.1 400 Bad Request"); + print "ERROR: " . t("Invalid upload"); + } + } + + public function status($success_count, $error_count) { + if ($error_count) { + // The "errors" won't be properly pluralized :-/ + print t2("Uploaded %count photo (%error errors)", + "Uploaded %count photos (%error errors)", + (int)$success_count, + array("error" => (int)$error_count)); + } else { + print t2("Uploaded %count photo", "Uploaded %count photos", $success_count);} + } + + public function finish() { + access::verify_csrf(); + + batch::stop(); + json::reply(array("result" => "success")); + } + + private function _get_add_form($album) { + $form = new Forge("uploader/finish", "", "post", array("id" => "g-add-photos-form")); + $group = $form->group("add_photos") + ->label(t("Add photos to %album_title", array("album_title" => html::purify($album->title)))); + $group->uploadify("uploadify")->album($album); + + $group = $form->group("actions"); + $group->uploadify_buttons(""); + + module::event("add_photos_form", $album, $form); + + return $form; + } +} diff --git a/modules/gallery/controllers/user_profile.php b/modules/gallery/controllers/user_profile.php new file mode 100644 index 0000000..cf589da --- /dev/null +++ b/modules/gallery/controllers/user_profile.php @@ -0,0 +1,108 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class User_Profile_Controller extends Controller { + + public function show($id) { + // If we get here, then we should have a user id other than guest. + $user = identity::lookup_user($id); + if (!$user) { + throw new Kohana_404_Exception(); + } + + if (!$this->_can_view_profile_pages($user)) { + throw new Kohana_404_Exception(); + } + + $v = new Theme_View("page.html", "other", "profile"); + $v->page_title = t("%name Profile", array("name" => $user->display_name())); + $v->content = new View("user_profile.html"); + + $v->content->user = $user; + $v->content->contactable = + !$user->guest && $user->id != identity::active_user()->id && $user->email; + $v->content->editable = + identity::is_writable() && !$user->guest && $user->id == identity::active_user()->id; + + $event_data = (object)array("user" => $user, "content" => array()); + module::event("show_user_profile", $event_data); + $v->content->info_parts = $event_data->content; + + print $v; + } + + public function contact($id) { + $user = identity::lookup_user($id); + if (!$this->_can_view_profile_pages($user)) { + throw new Kohana_404_Exception(); + } + + print user_profile::get_contact_form($user); + } + + public function send($id) { + access::verify_csrf(); + $user = identity::lookup_user($id); + if (!$this->_can_view_profile_pages($user)) { + throw new Kohana_404_Exception(); + } + + $form = user_profile::get_contact_form($user); + if ($form->validate()) { + Sendmail::factory() + ->to($user->email) + ->subject(html::clean($form->message->subject->value)) + ->header("Mime-Version", "1.0") + ->header("Content-type", "text/html; charset=UTF-8") + ->reply_to($form->message->reply_to->value) + ->message(html::purify($form->message->message->value)) + ->send(); + message::success(t("Sent message to %user_name", array("user_name" => $user->display_name()))); + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + private function _can_view_profile_pages($user) { + if (!$user->loaded()) { + return false; + } + + if ($user->id == identity::active_user()->id) { + // You can always view your own profile + return true; + } + + switch (module::get_var("gallery", "show_user_profiles_to")) { + case "admin_users": + return identity::active_user()->admin; + + case "registered_users": + return !identity::active_user()->guest; + + case "everybody": + return true; + + default: + // Fail in private mode on an invalid setting + return false; + } + } +} diff --git a/modules/gallery/controllers/welcome_message.php b/modules/gallery/controllers/welcome_message.php new file mode 100644 index 0000000..bde5e94 --- /dev/null +++ b/modules/gallery/controllers/welcome_message.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Welcome_Message_Controller extends Controller { + public function index() { + if (!identity::active_user()->admin) { + url::redirect(item::root()->abs_url()); + } + + $v = new View("welcome_message.html"); + $v->user = identity::active_user(); + print $v; + } +} diff --git a/modules/gallery/css/debug.css b/modules/gallery/css/debug.css new file mode 100644 index 0000000..6808da0 --- /dev/null +++ b/modules/gallery/css/debug.css @@ -0,0 +1,28 @@ +.g-annotated-theme-block { + border: 1px solid #C00; + clear: both; + margin: 1em; + padding: 1em; + position: relative; +} + +.g-annotated-theme-block_album_top { + float: right; +} + +.g-annotated-theme-block_header_bottom { + float: right; +} + +.g-annotated-theme-block div.title { + background: #C00; + border: 1px solid black; + color: white; + font-size: 110%; + padding: 4px; + position: absolute; + right: -1em; + top: -1em; + text-align: left; + -moz-border-radius: 5%; +} diff --git a/modules/gallery/css/gallery.css b/modules/gallery/css/gallery.css new file mode 100644 index 0000000..7e71115 --- /dev/null +++ b/modules/gallery/css/gallery.css @@ -0,0 +1,213 @@ +/** + * Gallery 3 core module styles + * + * Sheet organization: + * 1) End-user + * 2) Admin + * 3) Right to left language styles + */ + +/** ******************************************************************* + * 1) End-user + **********************************************************************/ + +/* Uploader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-add-photos-canvas, +#g-add-photos-status { + width: 469px; +} + +#g-add-photos-canvas { + border: 1px solid #ccc; + height: 200px; + margin: .5em 0; + padding: 2.8em 0 0 0; + overflow: auto; + position: relative; +} + +#g-add-photos-canvas object, +#g-add-photos-button { + left: 90px; + margin: .5em 0; + padding: .4em 1em; + position: absolute; + top: 0; + width: 300px; +} + +#g-add-photos-canvas object { + z-index: 100; + padding: 0em; +} + +#g-add-photos-canvas .uploadifyQueue { + margin-top: .5em; +} + +#g-add-photos-canvas .uploadifyQueueItem { + margin: 0; +} + +#g-add-photos-button { + z-index: 1; +} + +#g-add-photos-status { + border: 1px solid #ccc; + height: 125px; + margin: .4em 0; + overflow: auto; +} + +#g-add-photos-status .g-message-block { + border: none; +} + +#g-add-photos-status #g-action-status li { + margin: 0 0 1px 0; + padding-top: .7em; + width: 433px; +} + +#g-add-photos-form .g-breadcrumbs { + margin: 0; +} + +#g-add-photos-form p { + margin-bottom: 0 +} + +#g-add-photos-status-message { + float: right; +} + +/* Permissions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-edit-permissions-form { + clear: both; +} + +#g-edit-permissions-form th { + text-align: center; +} + +#g-permissions .g-denied, +#g-permissions .g-allowed { + text-align: center; + vertical-align: middle; +} + +/* Move items ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-move ul { + padding-left: 1em; +} + +#g-move .selected { + background: #999; +} + +/* In-place edit ~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-in-place-edit-form ul { + margin: 0; +} + +/* User profile ~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-user-profile h1 { + margin: 1em 0; +} + +#g-user-profile .g-avatar { + margin-right: .6em; +} + +#g-user-profile .g-block { + margin-top: 0; +} + +#g-user-profile .g-block-content { + margin-top: 0; +} + +#g-user-profile th, +#g-user-profile td { + border: none; +} + +#g-user-profile th { + white-space: nowrap; + width: 1%; +} + +/* Unsupported movie download link ~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +.g-movie-download-link { + text-align: center; +} + +/** ******************************************************************* + * 2) Admin + **********************************************************************/ + +.g-task-log { + border: 1px solid #000; + height: 400px; + margin: .6em 0; + overflow: auto; + padding: .4em +} + +#g-languages-form table { + width: 40%; + margin: 0 3em 1em 0; +} + +#g-languages-form input { + clear: both; +} + +#g-translations ol { + margin: 0 0 1em 2em; +} +#g-translations ol li { + list-style-type: decimal; + line-height: 150%; +} + +#g-translations .g-button { + padding: .5em; + margin-bottom: 1em; +} + +/** ******************************************************************* + * 3) Right to left language styles + **********************************************************************/ + +.rtl #g-add-photos-status #g-action-status li { + width: 407px; +} + +.rtl #g-block-admin .g-left { + margin-left: 1em; + margin-right: 0; +} + +.rtl #g-user-profile .g-avatar { + margin-left: .6em; +} + +.rtl #g-languages-form table { + margin: 0 0 1em 3em; +} + +.rtl #g-translations ol { + margin: 0 2em 1em 0; +} + +.rtl #g-add-photos-status-message { + float: left; +} diff --git a/modules/gallery/css/l10n_client.css b/modules/gallery/css/l10n_client.css new file mode 100644 index 0000000..90034d0 --- /dev/null +++ b/modules/gallery/css/l10n_client.css @@ -0,0 +1,206 @@ +/** + * TODO(andy_st): Add original copyright notice from Drupal l10_client. + * TODO(andy_st): Add G3 copyright notice. + * TODO(andy_st): clean up formatting to match our other CSS files. + */ + +/* $Id: l10n_client.css,v 1.6 2008/09/09 10:48:20 goba Exp $ */ + +/* width percentages add to 99% rather than 100% to prevent float + overflows from occurring in an unnamed browser that can't decide + how it wants to round. */ + +/* l10n_client container */ +#l10n-client { + text-align:left; + z-index:99; + line-height:1em; + color:#000; background:#fff; + position:fixed; + width:100%; height: 2em; + bottom:0px; left:0px; + overflow:hidden;} + +* html #l10n-client { + position:static;} + +#l10n-client-string-select .string-list, +#l10n-client-string-editor .source, +#l10n-client-string-editor .editor { + height:20em;} + +#l10n-client .labels { + overflow:hidden; + position:relative; + height:2em; + color:#fff; + background:#37a;} + +#l10n-client .labels .label { + display:none;} + +/* Panel toggle button (span) */ +#l10n-client-toggler { + cursor:pointer; + display:block; + position:absolute; right:0em; + height:2em; line-height:2em; + text-align:center; background:#000; +} +#l10n-client-toggler a { + font-size: 1em; + padding: .5em; +} +#l10n-client-toggler #g-minimize-l10n { + border-right: 1px solid #ffffff; +} + +/* Panel labels */ +#l10n-client h2 { + border-left:1px solid #fff; + height:1em; line-height:1em; + padding: .5em; margin:0px; + font-size:1em; +} + +#l10n-client .strings h2 { + border:0px;} + +/* 25 + 37 + 37 = 99 */ +#l10n-client .strings { + width:25%; float:left;} + +#l10n-client .source { + width:37%; float:left;} + +#l10n-client .translation { + width:37%; float:left;} + +/* Translatable string list */ +#l10n-client-string-select { + display:none; + float:left; + width:25%; + direction: ltr; +} + +#l10n-client .string-list { + height: 16em; + overflow:auto; + list-style:none; list-style-image:none; + margin:0em; padding:0em;} + +#l10n-client .string-list li { + font-size:.9em; + line-height:1.5em; + cursor:default; + background:transparent; + list-style:none; list-style-image:none; + border-bottom:1px solid #ddd; + padding:.25em .5em; + margin:0em;} + +/* Green for translated */ +#l10n-client .string-list li.translated { + border-bottom-color:#9c3; + background:#cf6; color:#360;} + +#l10n-client .string-list li.translated:hover { + background: #df8;} + +#l10n-client .string-list li.translated:active { + background: #9c3;} + +#l10n-client .string-list li.hidden { + display:none;} + +/* Gray + Blue hover for untranslated */ +#l10n-client .string-list li.untranslated {} + +#l10n-client .string-list li.untranslated:hover { + background: #ace;} + +#l10n-client .string-list li.untranslated:active { + background: #8ac;} + +/* Selected string is indicated by bold text */ +#l10n-client .string-list li.active { + font-weight:bold;} + +#l10n-client #g-l10n-search-form { + background: #eee; + margin: 0em; + padding: .25em .25em; +} + +#l10n-client #g-l10n-search-form .form-item, +#l10n-client #g-l10n-search-form input.form-text, +#l10n-client #g-l10n-search-form #search-filter-go, +#l10n-client #g-l10n-search-form #search-filter-clear { + display:inline; + vertical-align:middle; +} + +#l10n-client #g-l10n-search-form .form-item { + margin:0em; + padding:0em; +} + +#l10n-client #g-l10n-search-form fieldset { + margin-bottom: 0; + padding: .25em .25em; +} + + +#l10n-client #g-l10n-search-form input { + width: 96.75%; +} + +#l10n-client #g-l10n-search-form #search-filter-clear { + width:10%; + margin:0em; +} + +#l10n-client-string-editor { + display:none; + float:left; + width:74%;} + +#l10n-client-string-editor .source { + overflow:hidden; + width:50%; + float:left; +} + +#l10n-client-string-editor .source .source-text { + line-height:1.5em; + background:#eee; + font-family: monospace; + text-align: left; + height:16em; margin:1em; padding:1em; + overflow:auto; + direction: ltr; +} + +#l10n-client-string-editor .translation { + overflow-y:auto; + overflow-x: hidden; + height: 20em; + width:49%; + float: right; +} + +#g-l10n-client-save-form { + padding: 0em; +} + +#l10n-client form ul, +#l10n-client form li, +#l10n-client form input[type=submit], +#l10n-client form input[type=text] { + display: inline ! important ; +} + +#l10n-client form .hidden { + display: none; +} diff --git a/modules/gallery/css/upgrader.css b/modules/gallery/css/upgrader.css new file mode 100644 index 0000000..8610016 --- /dev/null +++ b/modules/gallery/css/upgrader.css @@ -0,0 +1,183 @@ +body { + background: #eee; + font-family: 'Trebuchet MS'; + font-size: 1.1em; +} + +h1 { + font-size: 1.4em; +} + +div#outer { + background: #fff; + border: 1px solid #999; + margin: 0 auto; + width: 650px; +} + +div#inner { + margin: 0; + padding: 0 1em; +} + +div#footer { + border-top: 1px solid #ccc; + margin: 1em; +} + +table { + width: 600px; + margin-bottom: 10px; +} + +th.name, +td.name { + text-align: left; + padding-left: 30px; +} + +td { + text-align: center; + border-bottom: 1px solid #eee; +} + +tr.current td { + opacity: 0.5; + font-style: italic; +} + +tr.current td.gallery { + color: #00d; +} + +tr.upgradeable td { + font-weight: bold; +} + +tr.upgradeable td.gallery { + color: #00d; +} + +tr.failed td { + color: red; +} + +p { + font-size: .9em; +} + +ul { + font-size: .9em; + list-style: none; +} + +li { + display: inline; +} + +li:before { + content: "\00BB \0020"; +} + +div.button { + margin: 0 auto; + width: 120px; + text-align: center; + border: 1px solid #999; + background: #eee; +} + +div.button a { + text-decoration: none; +} + +div.button-active:hover { + background: #ccc; +} + +div#dialog { + width: 340px; + height: 200px; + position: absolute; + background: blue; + z-index: 1000; + margin: 10px; + text-align: center; +} + +div#dialog a.close { + float: right; + padding: 10px; + text-decoration: none; +} + +div#dialog div { + width: 292px; + height: 152px; + margin: 2px; + padding: 20px; + border: 2px solid #999; + background: #eee; +} + +.muted { + opacity: 0.5; +} + +.failed { + color: red; +} + +pre { + display: inline; + margin: 0px; + padding: 0px; +} + +div#upgrade_button { + margin-bottom: 20px; +} + +div#welcome_message { + margin-left: 30px; +} + +#logo { + margin-left: 14px; +} + +.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; +} + +.rtl div#welcome_message { + padding-right: 30px; +} + +.rtl #logo { + padding-right: 12px; +} diff --git a/modules/gallery/helpers/MY_html.php b/modules/gallery/helpers/MY_html.php new file mode 100644 index 0000000..767fe3f --- /dev/null +++ b/modules/gallery/helpers/MY_html.php @@ -0,0 +1,91 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class html extends html_Core { + /** + * Returns a string that is safe to be used in HTML (XSS protection). + * + * If $html is a string, the returned string will be HTML escaped. + * If $html is a SafeString instance, the returned string may contain + * unescaped HTML which is assumed to be safe. + * + * Example:<pre> + * <div><?= html::clean($php_var) ?> + * </pre> + */ + static function clean($html) { + return new SafeString($html); + } + + /** + * Returns a string that is safe to be used in HTML (XSS protection), + * purifying (filtering) the given HTML to ensure that the result contains + * only non-malicious HTML. + * + * Example:<pre> + * <div><?= html::purify($item->title) ?> + * </pre> + */ + static function purify($html) { + return SafeString::purify($html); + } + + /** + * Flags the given string as safe to be used in HTML (free of malicious HTML/JS). + * + * Example:<pre> + * // Parameters to t() are automatically escaped by default. + * // If the parameter is marked as clean, it won't get escaped. + * t('Go <a href="%url">there</a>', + * array("url" => html::mark_clean(url::current()))) + * </pre> + */ + static function mark_clean($html) { + return SafeString::of_safe_html($html); + } + + /** + * Escapes the given string for use in JavaScript. + * + * Example:<pre> + * <script type="text/javascript>" + * var some_js_string = <?= html::js_string($php_string) ?>; + * </script> + * </pre> + */ + static function js_string($string) { + return SafeString::of($string)->for_js(); + } + + /** + * Returns a string safe for use in HTML element attributes. + * + * Assumes that the HTML element attribute is already + * delimited by single or double quotes + * + * Example:<pre> + * <a title="<?= html::clean_for_attribute($php_var) ?>">; + * </script> + * </pre> + * @return the string escaped for use in HTML attributes. + */ + static function clean_attribute($string) { + return html::clean($string)->for_html_attr(); + } +} diff --git a/modules/gallery/helpers/MY_num.php b/modules/gallery/helpers/MY_num.php new file mode 100644 index 0000000..a550a1a --- /dev/null +++ b/modules/gallery/helpers/MY_num.php @@ -0,0 +1,54 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class num extends num_Core { + /** + * Convert a size value as accepted by PHP's shorthand to bytes. + * ref: http://us2.php.net/manual/en/function.ini-get.php + * ref: http://us2.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + */ + static function convert_to_bytes($val) { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + switch($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } + + /** + * Convert a size value as accepted by PHP's shorthand to bytes. + * ref: http://us2.php.net/manual/en/function.ini-get.php + * ref: http://us2.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + */ + static function convert_to_human_readable($num) { + foreach (array("G" => 1e9, "M" => 1e6, "K" => 1e3) as $k => $v) { + if ($num > $v) { + $num = round($num / $v) . $k; + } + } + return $num; + } +} diff --git a/modules/gallery/helpers/MY_remote.php b/modules/gallery/helpers/MY_remote.php new file mode 100644 index 0000000..59804b9 --- /dev/null +++ b/modules/gallery/helpers/MY_remote.php @@ -0,0 +1,167 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class remote extends remote_Core { + + static function post($url, $post_data_array, $extra_headers=array()) { + $post_data_raw = self::_encode_post_data($post_data_array, $extra_headers); + + /* Read the web page into a buffer */ + list ($response_status, $response_headers, $response_body) = + remote::do_request($url, 'POST', $extra_headers, $post_data_raw); + + return array($response_body, $response_status, $response_headers); + } + + static function success($response_status) { + return preg_match("/^HTTP\/\d+\.\d+\s2\d{2}(\s|$)/", trim($response_status)); + } + + /** + * Encode the post data. For each key/value pair, urlencode both the key and the value and then + * concatenate together. As per the specification, each key/value pair is separated with an + * ampersand (&) + * @param array $post_data_array the key/value post data + * @param array $extra_headers extra headers to pass to the server + * @return string the encoded post data + */ + private static function _encode_post_data($post_data_array, &$extra_headers) { + $post_data_raw = ''; + foreach ($post_data_array as $key => $value) { + if (!empty($post_data_raw)) { + $post_data_raw .= '&'; + } + $post_data_raw .= urlencode($key) . '=' . urlencode($value); + } + + $extra_headers['Content-Type'] = 'application/x-www-form-urlencoded'; + $extra_headers['Content-Length'] = strlen($post_data_raw); + + return $post_data_raw; + } + + /** + * A single request, without following redirects + * + * @todo: Handle redirects? If so, only for GET (i.e. not for POST), and use G2's + * WebHelper_simple::_parseLocation logic. + */ + static function do_request($url, $method='GET', $headers=array(), $body='') { + if (!array_key_exists("User-Agent", $headers)) { + $headers["User-Agent"] = "Gallery3"; + } + /* Convert illegal characters */ + $url = str_replace(' ', '%20', $url); + + $url_components = self::_parse_url_for_fsockopen($url); + $handle = fsockopen( + $url_components['fsockhost'], $url_components['port'], $errno, $errstr, 5); + if (empty($handle)) { + // log "Error $errno: '$errstr' requesting $url"; + return array(null, null, null); + } + + $header_lines = array('Host: ' . $url_components['host']); + foreach ($headers as $key => $value) { + $header_lines[] = $key . ': ' . $value; + } + + $success = fwrite($handle, sprintf("%s %s HTTP/1.0\r\n%s\r\n\r\n%s", + $method, + $url_components['uri'], + implode("\r\n", $header_lines), + $body)); + if (!$success) { + // Zero bytes written or false was returned + // log "fwrite failed in requestWebPage($url)" . ($success === false ? ' - false' : '' + return array(null, null, null); + } + fflush($handle); + + /* + * Read the status line. fgets stops after newlines. The first line is the protocol + * version followed by a numeric status code and its associated textual phrase. + */ + $response_status = trim(fgets($handle, 4096)); + if (empty($response_status)) { + // 'Empty http response code, maybe timeout' + return array(null, null, null); + } + + /* Read the headers */ + $response_headers = array(); + while (!feof($handle)) { + $line = trim(fgets($handle, 4096)); + if (empty($line)) { + break; + } + + /* Normalize the line endings */ + $line = str_replace("\r", '', $line); + + list ($key, $value) = explode(':', $line, 2); + if (isset($response_headers[$key])) { + if (!is_array($response_headers[$key])) { + $response_headers[$key] = array($response_headers[$key]); + } + $response_headers[$key][] = trim($value); + } else { + $response_headers[$key] = trim($value); + } + } + + /* Read the body */ + $response_body = ''; + while (!feof($handle)) { + $response_body .= fread($handle, 4096); + } + fclose($handle); + + return array($response_status, $response_headers, $response_body); + } + + /** + * Prepare for fsockopen call. + * @param string $url + * @return array url components + * @access private + */ + private static function _parse_url_for_fsockopen($url) { + $url_components = parse_url($url); + if (strtolower($url_components['scheme']) == 'https') { + $url_components['fsockhost'] = 'ssl://' . $url_components['host']; + $default_port = 443; + } else { + $url_components['fsockhost'] = $url_components['host']; + $default_port = 80; + } + if (empty($url_components['port'])) { + $url_components['port'] = $default_port; + } + if (empty($url_components['path'])) { + $url_components['path'] = '/'; + } + $uri = $url_components['path'] + . (empty($url_components['query']) ? '' : '?' . $url_components['query']); + /* Unescape ampersands, since if the url comes from form input it will be escaped */ + $url_components['uri'] = str_replace('&', '&', $uri); + return $url_components; + } +} + diff --git a/modules/gallery/helpers/MY_url.php b/modules/gallery/helpers/MY_url.php new file mode 100644 index 0000000..eba08b2 --- /dev/null +++ b/modules/gallery/helpers/MY_url.php @@ -0,0 +1,92 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class url extends url_Core { + static function parse_url() { + if (Router::$controller) { + return; + } + + // Work around problems with the CGI sapi by enforcing our default path + if ($_SERVER["SCRIPT_NAME"] && "/" . Router::$current_uri == $_SERVER["SCRIPT_NAME"]) { + Router::$controller_path = MODPATH . "gallery/controllers/albums.php"; + Router::$controller = "albums"; + Router::$method = 1; + return; + } + + $item = item::find_by_relative_url(html_entity_decode(Router::$current_uri, ENT_QUOTES)); + if ($item && $item->loaded()) { + Router::$controller = "{$item->type}s"; + Router::$controller_path = MODPATH . "gallery/controllers/{$item->type}s.php"; + Router::$method = "show"; + Router::$arguments = array($item); + } + } + + /** + * Just like url::file() except that it returns an absolute URI + */ + static function abs_file($path) { + return url::base(false, request::protocol()) . $path; + } + + /** + * Just like url::site() except that it returns an absolute URI and + * doesn't take a protocol parameter. + */ + static function abs_site($path) { + return url::site($path, request::protocol()); + } + + /** + * Just like url::current except that it returns an absolute URI + */ + static function abs_current($qs=false) { + return self::abs_site(url::current($qs)); + } + + /** + * Just like url::merge except that it escapes any XSS in the path. + */ + static function merge(array $arguments) { + return htmlspecialchars(parent::merge($arguments)); + } + + /** + * Just like url::current except that it escapes any XSS in the path. + */ + static function current($qs=false, $suffix=false) { + return htmlspecialchars(parent::current($qs, $suffix)); + } + + /** + * Merge extra an query string onto a given url safely. + * @param string the original url + * @param array the query string data in key=value form + */ + static function merge_querystring($url, $query_params) { + $qs = implode("&", $query_params); + if (strpos($url, "?") === false) { + return $url . "?$qs"; + } else { + return $url . "&$qs"; + } + } +} diff --git a/modules/gallery/helpers/MY_valid.php b/modules/gallery/helpers/MY_valid.php new file mode 100644 index 0000000..f1dd9c3 --- /dev/null +++ b/modules/gallery/helpers/MY_valid.php @@ -0,0 +1,26 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class valid extends valid_Core { + static function url($url) { + return valid_Core::url($url) && + (!strncasecmp($url, "http://", strlen("http://")) || + !strncasecmp($url, "https://", strlen("https://"))); + } +} diff --git a/modules/gallery/helpers/access.php b/modules/gallery/helpers/access.php new file mode 100644 index 0000000..a7dca57 --- /dev/null +++ b/modules/gallery/helpers/access.php @@ -0,0 +1,758 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/** + * API for Gallery Access control. + * + * Permissions are hierarchical, and apply only to groups and albums. They cascade down from the + * top of the Gallery to the bottom, so if you set a permission in the root album, that permission + * applies for any sub-album unless the sub-album overrides it. Likewise, any permission applied + * to an album applies to any photos inside the album. Overrides can be applied at any level of + * the hierarchy for any permission other than View permissions. + * + * View permissions are an exceptional case. In the case of viewability, we want to ensure that + * if an album's parent is inaccessible, then this album must be inaccessible also. So while view + * permissions cascade downwards and you're free to set the ALLOW permission on any album, that + * ALLOW permission will be ignored unless all that album's parents are marked ALLOW also. + * + * Implementatation Notes: + * + * Notes refer to this example album hierarchy: + * A1 + * / \ + * A2 A3 + * / \ + * A4 A5 + * + * o We have the concept of "intents". A user can specify that he intends for A3 to be + * inaccessible (ie: a DENY on the "view" permission to the EVERYBODY group). Once A3 is + * inaccessible, A5 can never be displayed to that group. If A1 is made inaccessible, then the + * entire tree is hidden. If subsequently A1 is made accessible, then the whole tree is + * available again *except* A3 and below since the user's "intent" for A3 is maintained. + * + * o Intents are specified as <group_id, perm, item_id> tuples. It would be inefficient to check + * these tuples every time we want to do a lookup, so we use these intents to create an entire + * table of permissions for easy lookup in the Access_Cache_Model. There's a 1:1 mapping + * between Item_Model and Access_Cache_Model entries. + * + * o For efficiency, we create columns in Access_Intent_Model and Access_Cache_Model for each of + * the possible Group_Model and Permission_Model combinations. This may lead to performance + * issues for very large Gallery installs, but for small to medium sized ones (5-10 groups, 5-10 + * permissions) it's especially efficient because there's a single field value for each + * group/permission/item combination. + * + * o For efficiency, we store the cache columns for view permissions directly in the Item_Model. + * This means that we can filter items by group/permission combination without doing any table + * joins making for an especially efficient permission check at the expense of having to + * maintain extra columns for each item. + * + * o If at any time the Access_Cache_Model becomes invalid, we can rebuild the entire table from + * the Access_Intent_Model + */ +class access_Core { + const DENY = "0"; + const ALLOW = "1"; + const INHERIT = null; // access_intent + const UNKNOWN = null; // cache (access_cache, items) + + /** + * Does the active user have this permission on this item? + * + * @param string $perm_name + * @param Item_Model $item + * @return boolean + */ + static function can($perm_name, $item) { + return access::user_can(identity::active_user(), $perm_name, $item); + } + + /** + * Does the user have this permission on this item? + * + * @param User_Model $user + * @param string $perm_name + * @param Item_Model $item + * @return boolean + */ + static function user_can($user, $perm_name, $item) { + if (!$item->loaded()) { + return false; + } + + if ($user->admin) { + return true; + } + + // Use the nearest parent album (including the current item) so that we take advantage + // of the cache when checking many items in a single album. + $id = ($item->type == "album") ? $item->id : $item->parent_id; + $resource = $perm_name == "view" ? + $item : model_cache::get("access_cache", $id, "item_id"); + + foreach ($user->groups() as $group) { + if ($resource->__get("{$perm_name}_{$group->id}") === access::ALLOW) { + return true; + } + } + return false; + } + + /** + * If the active user does not have this permission, failed with an access::forbidden(). + * + * @param string $perm_name + * @param Item_Model $item + * @return boolean + */ + static function required($perm_name, $item) { + if (!access::can($perm_name, $item)) { + if ($perm_name == "view") { + // Treat as if the item didn't exist, don't leak any information. + throw new Kohana_404_Exception(); + } else { + access::forbidden(); + } + } + } + + /** + * Does this group have this permission on this item? + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + * @return boolean + */ + static function group_can($group, $perm_name, $item) { + // Use the nearest parent album (including the current item) so that we take advantage + // of the cache when checking many items in a single album. + $id = ($item->type == "album") ? $item->id : $item->parent_id; + $resource = $perm_name == "view" ? + $item : model_cache::get("access_cache", $id, "item_id"); + + return $resource->__get("{$perm_name}_{$group->id}") === access::ALLOW; + } + + /** + * Return this group's intent for this permission on this item. + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + * @return boolean access::ALLOW, access::DENY or access::INHERIT (null) for no intent + */ + static function group_intent($group, $perm_name, $item) { + $intent = model_cache::get("access_intent", $item->id, "item_id"); + return $intent->__get("{$perm_name}_{$group->id}"); + } + + /** + * Is the permission on this item locked by a parent? If so return the nearest parent that + * locks it. + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + * @return ORM_Model item that locks this one + */ + static function locked_by($group, $perm_name, $item) { + if ($perm_name != "view") { + return null; + } + + // For view permissions, if any parent is access::DENY, then those parents lock this one. + // Return + $lock = ORM::factory("item") + ->where("left_ptr", "<=", $item->left_ptr) + ->where("right_ptr", ">=", $item->right_ptr) + ->where("items.id", "<>", $item->id) + ->join("access_intents", "items.id", "access_intents.item_id") + ->where("access_intents.view_$group->id", "=", access::DENY) + ->order_by("level", "DESC") + ->limit(1) + ->find(); + + if ($lock->loaded()) { + return $lock; + } else { + return null; + } + } + + /** + * Terminate immediately with an HTTP 403 Forbidden response. + */ + static function forbidden() { + throw new Kohana_Exception("@todo FORBIDDEN", null, 403); + } + + /** + * Internal method to set a permission + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + * @param boolean $value + */ + private static function _set(Group_Definition $group, $perm_name, $album, $value) { + if (!($group instanceof Group_Definition)) { + throw new Exception("@todo PERMISSIONS_ONLY_WORK_ON_GROUPS"); + } + if (!$album->loaded()) { + throw new Exception("@todo INVALID_ALBUM $album->id"); + } + if (!$album->is_album()) { + throw new Exception("@todo INVALID_ALBUM_TYPE not an album"); + } + $access = model_cache::get("access_intent", $album->id, "item_id"); + $access->__set("{$perm_name}_{$group->id}", $value); + $access->save(); + + if ($perm_name == "view") { + self::_update_access_view_cache($group, $album); + } else { + self::_update_access_non_view_cache($group, $perm_name, $album); + } + + access::update_htaccess_files($album, $group, $perm_name, $value); + model_cache::clear(); + } + + /** + * Allow a group to have a permission on an item. + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + */ + static function allow($group, $perm_name, $item) { + self::_set($group, $perm_name, $item, self::ALLOW); + } + + /** + * Deny a group the given permission on an item. + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + */ + static function deny($group, $perm_name, $item) { + self::_set($group, $perm_name, $item, self::DENY); + } + + /** + * Unset the given permission for this item and use inherited values + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + */ + static function reset($group, $perm_name, $item) { + if ($item->id == 1) { + throw new Exception("@todo CANT_RESET_ROOT_PERMISSION"); + } + self::_set($group, $perm_name, $item, self::INHERIT); + } + + /** + * Recalculate the permissions for an album's hierarchy. + */ + static function recalculate_album_permissions($album) { + foreach (self::_get_all_groups() as $group) { + foreach (ORM::factory("permission")->find_all() as $perm) { + if ($perm->name == "view") { + self::_update_access_view_cache($group, $album); + } else { + self::_update_access_non_view_cache($group, $perm->name, $album); + } + } + } + model_cache::clear(); + } + + /** + * Recalculate the permissions for a single photo. + */ + static function recalculate_photo_permissions($photo) { + $parent = $photo->parent(); + $parent_access_cache = ORM::factory("access_cache")->where("item_id", "=", $parent->id)->find(); + $photo_access_cache = ORM::factory("access_cache")->where("item_id", "=", $photo->id)->find(); + foreach (self::_get_all_groups() as $group) { + foreach (ORM::factory("permission")->find_all() as $perm) { + $field = "{$perm->name}_{$group->id}"; + if ($perm->name == "view") { + $photo->$field = $parent->$field; + } else { + $photo_access_cache->$field = $parent_access_cache->$field; + } + } + } + $photo_access_cache->save(); + $photo->save(); + model_cache::clear(); + } + + /** + * Register a permission so that modules can use it. + * + * @param string $name The internal name for for this permission + * @param string $display_name The internationalized version of the displayable name + * @return void + */ + static function register_permission($name, $display_name) { + $permission = ORM::factory("permission", $name); + if ($permission->loaded()) { + throw new Exception("@todo PERMISSION_ALREADY_EXISTS $name"); + } + $permission->name = $name; + $permission->display_name = $display_name; + $permission->save(); + + foreach (self::_get_all_groups() as $group) { + self::_add_columns($name, $group); + } + } + + /** + * Delete a permission. + * + * @param string $perm_name + * @return void + */ + static function delete_permission($name) { + foreach (self::_get_all_groups() as $group) { + self::_drop_columns($name, $group); + } + $permission = ORM::factory("permission")->where("name", "=", $name)->find(); + if ($permission->loaded()) { + $permission->delete(); + } + } + + /** + * Add the appropriate columns for a new group + * + * @param Group_Model $group + * @return void + */ + static function add_group($group) { + foreach (ORM::factory("permission")->find_all() as $perm) { + self::_add_columns($perm->name, $group); + } + } + + /** + * Remove a group's permission columns (usually when it's deleted) + * + * @param Group_Model $group + * @return void + */ + static function delete_group($group) { + foreach (ORM::factory("permission")->find_all() as $perm) { + self::_drop_columns($perm->name, $group); + } + } + + /** + * Add new access rows when a new item is added. + * + * @param Item_Model $item + * @return void + */ + static function add_item($item) { + $access_intent = ORM::factory("access_intent", $item->id); + if ($access_intent->loaded()) { + throw new Exception("@todo ITEM_ALREADY_ADDED $item->id"); + } + $access_intent = ORM::factory("access_intent"); + $access_intent->item_id = $item->id; + $access_intent->save(); + + // Create a new access cache entry and copy the parents values. + $access_cache = ORM::factory("access_cache"); + $access_cache->item_id = $item->id; + if ($item->id != 1) { + $parent_access_cache = + ORM::factory("access_cache")->where("item_id", "=", $item->parent()->id)->find(); + foreach (self::_get_all_groups() as $group) { + foreach (ORM::factory("permission")->find_all() as $perm) { + $field = "{$perm->name}_{$group->id}"; + if ($perm->name == "view") { + $item->$field = $item->parent()->$field; + } else { + $access_cache->$field = $parent_access_cache->$field; + } + } + } + } + $item->save(); + $access_cache->save(); + } + + /** + * Delete appropriate access rows when an item is deleted. + * + * @param Item_Model $item + * @return void + */ + static function delete_item($item) { + ORM::factory("access_intent")->where("item_id", "=", $item->id)->find()->delete(); + ORM::factory("access_cache")->where("item_id", "=", $item->id)->find()->delete(); + } + + /** + * Verify our Cross Site Request Forgery token is valid, else throw an exception. + */ + static function verify_csrf() { + $input = Input::instance(); + if ($input->post("csrf", $input->get("csrf", null)) !== Session::instance()->get("csrf")) { + access::forbidden(); + } + } + + /** + * Get the Cross Site Request Forgery token for this session. + * @return string + */ + static function csrf_token() { + $session = Session::instance(); + $csrf = $session->get("csrf"); + if (empty($csrf)) { + $csrf = random::hash(); + $session->set("csrf", $csrf); + } + return $csrf; + } + + /** + * Generate an <input> element containing the Cross Site Request Forgery token for this session. + * @return string + */ + static function csrf_form_field() { + return "<input type=\"hidden\" name=\"csrf\" value=\"" . access::csrf_token() . "\"/>"; + } + + /** + * Internal method to get all available groups. + * + * @return ORM_Iterator + */ + private static function _get_all_groups() { + // When we build the gallery package, it's possible that there is no identity provider + // installed yet. This is ok at packaging time, so work around it. + if (module::is_active(module::get_var("gallery", "identity_provider", "user"))) { + return identity::groups(); + } else { + return array(); + } + } + + /** + * Internal method to remove Permission/Group columns + * + * @param Group_Model $group + * @param string $perm_name + * @return void + */ + private static function _drop_columns($perm_name, $group) { + $field = "{$perm_name}_{$group->id}"; + $cache_table = $perm_name == "view" ? "items" : "access_caches"; + Database::instance()->query("ALTER TABLE {{$cache_table}} DROP `$field`"); + Database::instance()->query("ALTER TABLE {access_intents} DROP `$field`"); + model_cache::clear(); + ORM::factory("access_intent")->clear_cache(); + } + + /** + * Internal method to add Permission/Group columns + * + * @param Group_Model $group + * @param string $perm_name + * @return void + */ + private static function _add_columns($perm_name, $group) { + $field = "{$perm_name}_{$group->id}"; + $cache_table = $perm_name == "view" ? "items" : "access_caches"; + $not_null = $cache_table == "items" ? "" : "NOT NULL"; + Database::instance()->query( + "ALTER TABLE {{$cache_table}} ADD `$field` BINARY $not_null DEFAULT FALSE"); + Database::instance()->query( + "ALTER TABLE {access_intents} ADD `$field` BINARY DEFAULT NULL"); + db::build() + ->update("access_intents") + ->set($field, access::DENY) + ->where("item_id", "=", 1) + ->execute(); + model_cache::clear(); + ORM::factory("access_intent")->clear_cache(); + } + + /** + * Update the Access_Cache model based on information from the Access_Intent model for view + * permissions only. + * + * @todo: use database locking + * + * @param Group_Model $group + * @param Item_Model $item + * @return void + */ + private static function _update_access_view_cache($group, $item) { + $access = ORM::factory("access_intent")->where("item_id", "=", $item->id)->find(); + $field = "view_{$group->id}"; + + // With view permissions, deny values in the parent can override allow values in the child, + // so start from the bottom of the tree and work upwards overlaying negative on top of + // positive. + // + // If the item's intent is ALLOW or DEFAULT, it's possible that some ancestor has specified + // DENY and this ALLOW cannot be obeyed. So in that case, back up the tree and find any + // non-DEFAULT and non-ALLOW parent and propagate from there. If we can't find a matching + // item, then its safe to propagate from here. + if ($access->$field !== access::DENY) { + $tmp_item = ORM::factory("item") + ->where("left_ptr", "<", $item->left_ptr) + ->where("right_ptr", ">", $item->right_ptr) + ->join("access_intents", "access_intents.item_id", "items.id") + ->where("access_intents.$field", "=", access::DENY) + ->order_by("left_ptr", "DESC") + ->limit(1) + ->find(); + if ($tmp_item->loaded()) { + $item = $tmp_item; + } + } + + // We will have a problem if we're trying to change a DENY to an ALLOW because the + // access_caches table will already contain DENY values and we won't be able to overwrite + // them according the rule above. So mark every permission below this level as UNKNOWN so + // that we can tell which permissions have been changed, and which ones need to be updated. + db::build() + ->update("items") + ->set($field, access::UNKNOWN) + ->where("left_ptr", ">=", $item->left_ptr) + ->where("right_ptr", "<=", $item->right_ptr) + ->execute(); + + $query = ORM::factory("access_intent") + ->select(array("access_intents.$field", "items.left_ptr", "items.right_ptr", "items.id")) + ->join("items", "items.id", "access_intents.item_id") + ->where("left_ptr", ">=", $item->left_ptr) + ->where("right_ptr", "<=", $item->right_ptr) + ->where("type", "=", "album") + ->where("access_intents.$field", "IS NOT", access::INHERIT) + ->order_by("level", "DESC") + ->find_all(); + foreach ($query as $row) { + if ($row->$field == access::ALLOW) { + // Propagate ALLOW for any row that is still UNKNOWN. + db::build() + ->update("items") + ->set($field, $row->$field) + ->where($field, "IS", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS + ->where("left_ptr", ">=", $row->left_ptr) + ->where("right_ptr", "<=", $row->right_ptr) + ->execute(); + } else if ($row->$field == access::DENY) { + // DENY overwrites everything below it + db::build() + ->update("items") + ->set($field, $row->$field) + ->where("left_ptr", ">=", $row->left_ptr) + ->where("right_ptr", "<=", $row->right_ptr) + ->execute(); + } + } + + // Finally, if our intent is DEFAULT at this point it means that we were unable to find a + // DENY parent in the hierarchy to propagate from. So we'll still have a UNKNOWN values in + // the hierarchy, and all of those are safe to change to ALLOW. + db::build() + ->update("items") + ->set($field, access::ALLOW) + ->where($field, "IS", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS + ->where("left_ptr", ">=", $item->left_ptr) + ->where("right_ptr", "<=", $item->right_ptr) + ->execute(); + } + + /** + * Update the Access_Cache model based on information from the Access_Intent model for non-view + * permissions. + * + * @todo: use database locking + * + * @param Group_Model $group + * @param string $perm_name + * @param Item_Model $item + * @return void + */ + private static function _update_access_non_view_cache($group, $perm_name, $item) { + $access = ORM::factory("access_intent")->where("item_id", "=", $item->id)->find(); + + $field = "{$perm_name}_{$group->id}"; + + // If the item's intent is DEFAULT, then we need to back up the chain to find the nearest + // parent with an intent and propagate from there. + // + // @todo To optimize this, we wouldn't need to propagate from the parent, we could just + // propagate from here with the parent's intent. + if ($access->$field === access::INHERIT) { + $tmp_item = ORM::factory("item") + ->join("access_intents", "items.id", "access_intents.item_id") + ->where("left_ptr", "<", $item->left_ptr) + ->where("right_ptr", ">", $item->right_ptr) + ->where($field, "IS NOT", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS NOT + ->order_by("left_ptr", "DESC") + ->limit(1) + ->find(); + if ($tmp_item->loaded()) { + $item = $tmp_item; + } + } + + // With non-view permissions, each level can override any permissions that came above it + // so start at the top and work downwards, overlaying permissions as we go. + $query = ORM::factory("access_intent") + ->select(array("access_intents.$field", "items.left_ptr", "items.right_ptr")) + ->join("items", "items.id", "access_intents.item_id") + ->where("left_ptr", ">=", $item->left_ptr) + ->where("right_ptr", "<=", $item->right_ptr) + ->where($field, "IS NOT", access::INHERIT) + ->order_by("level", "ASC") + ->find_all(); + foreach ($query as $row) { + $value = ($row->$field === access::ALLOW) ? true : false; + db::build() + ->update("access_caches") + ->set($field, $value) + ->where("item_id", "IN", + db::build() + ->select("id") + ->from("items") + ->where("left_ptr", ">=", $row->left_ptr) + ->where("right_ptr", "<=", $row->right_ptr)) + ->execute(); + } + } + + /** + * Rebuild the .htaccess files that prevent direct access to albums, resizes and thumbnails. We + * call this internally any time we change the view or view_full permissions for guest users. + * This function is only public because we use it in maintenance tasks. + * + * @param Item_Model the album + * @param Group_Model the group whose permission is changing + * @param string the permission name + * @param string the new permission value (eg access::DENY) + */ + static function update_htaccess_files($album, $group, $perm_name, $value) { + if ($group->id != identity::everybody()->id || + !($perm_name == "view" || $perm_name == "view_full")) { + return; + } + + $dirs = array($album->file_path()); + if ($perm_name == "view") { + $dirs[] = dirname($album->resize_path()); + $dirs[] = dirname($album->thumb_path()); + } + + $base_url = url::base(true); + $sep = "?"; + if (strpos($base_url, "?") !== false) { + $sep = "&"; + } + $base_url .= $sep . "kohana_uri=/file_proxy"; + // Replace "/index.php/?kohana..." with "/index.php?koahan..." + // Doesn't apply to "/?kohana..." or "/foo/?kohana..." + // Can't check for "index.php" since the file might be renamed, and + // there might be more Apache aliases / rewrites at work. + $url_path = parse_url($base_url, PHP_URL_PATH); + // Does the URL path have a file component? + if (preg_match("#[^/]+\.php#i", $url_path)) { + $base_url = str_replace("/?", "?", $base_url); + } + + foreach ($dirs as $dir) { + if ($value === access::DENY) { + $fp = fopen("$dir/.htaccess", "w+"); + fwrite($fp, "<IfModule mod_rewrite.c>\n"); + fwrite($fp, " RewriteEngine On\n"); + fwrite($fp, " RewriteRule (.*) $base_url/\$1 [L]\n"); + fwrite($fp, "</IfModule>\n"); + fwrite($fp, "<IfModule !mod_rewrite.c>\n"); + fwrite($fp, " Order Deny,Allow\n"); + fwrite($fp, " Deny from All\n"); + fwrite($fp, "</IfModule>\n"); + fclose($fp); + } else { + @unlink($dir . "/.htaccess"); + } + } + } + + static function private_key() { + return module::get_var("gallery", "private_key"); + } + + /** + * Verify that our htaccess based permission system actually works. Create a temporary + * directory containing an .htaccess file that uses mod_rewrite to redirect /verify to + * /success. Then request that url. If we retrieve it successfully, then our redirects are + * working and our permission system works. + */ + static function htaccess_works() { + $success_url = url::file("var/security_test/success"); + + @mkdir(VARPATH . "security_test"); + try { + if ($fp = @fopen(VARPATH . "security_test/.htaccess", "w+")) { + fwrite($fp, "Options +FollowSymLinks\n"); + fwrite($fp, "RewriteEngine On\n"); + fwrite($fp, "RewriteRule verify $success_url [L]\n"); + fclose($fp); + } + + if ($fp = @fopen(VARPATH . "security_test/success", "w+")) { + fwrite($fp, "success"); + fclose($fp); + } + + // Proxy our authorization headers so that if the entire Gallery is covered by Basic Auth + // this callback will still work. + $headers = array(); + if (function_exists("apache_request_headers")) { + $arh = apache_request_headers(); + if (!empty($arh["Authorization"])) { + $headers["Authorization"] = $arh["Authorization"]; + } + } + list ($status, $headers, $body) = + remote::do_request(url::abs_file("var/security_test/verify"), "GET", $headers); + $works = ($status == "HTTP/1.1 200 OK") && ($body == "success"); + } catch (Exception $e) { + @dir::unlink(VARPATH . "security_test"); + throw $e; + } + @dir::unlink(VARPATH . "security_test"); + + return $works; + } +} diff --git a/modules/gallery/helpers/ajax.php b/modules/gallery/helpers/ajax.php new file mode 100644 index 0000000..0c69fe7 --- /dev/null +++ b/modules/gallery/helpers/ajax.php @@ -0,0 +1,31 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class ajax_Core { + /** + * Encode an Ajax response so that it's UTF-7 safe. + * + * @param string $message string to print + */ + static function response($content) { + header("Content-Type: text/plain; charset=" . Kohana::CHARSET); + print "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n"; + print $content; + } +} diff --git a/modules/gallery/helpers/album.php b/modules/gallery/helpers/album.php new file mode 100644 index 0000000..23aed8a --- /dev/null +++ b/modules/gallery/helpers/album.php @@ -0,0 +1,126 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling albums. + * + * Note: by design, this class does not do any permission checking. + */ +class album_Core { + + static function get_add_form($parent) { + $form = new Forge("albums/create/{$parent->id}", "", "post", array("id" => "g-add-album-form")); + $group = $form->group("add_album") + ->label(t("Add an album to %album_title", array("album_title" => $parent->title))); + $group->input("title")->label(t("Title")) + ->error_messages("required", t("You must provide a title")) + ->error_messages("length", t("Your title is too long")); + $group->textarea("description")->label(t("Description")); + $group->input("name")->label(t("Directory name")) + ->error_messages("no_slashes", t("The directory name can't contain the \"/\" character")) + ->error_messages("required", t("You must provide a directory name")) + ->error_messages("length", t("Your directory name is too long")) + ->error_messages("conflict", t("There is already a movie, photo or album with this name")); + $group->input("slug")->label(t("Internet Address")) + ->error_messages( + "reserved", t("This address is reserved and can't be used.")) + ->error_messages( + "not_url_safe", + t("The internet address should contain only letters, numbers, hyphens and underscores")) + ->error_messages("required", t("You must provide an internet address")) + ->error_messages("length", t("Your internet address is too long")); + $group->hidden("type")->value("album"); + + module::event("album_add_form", $parent, $form); + + $group->submit("")->value(t("Create")); + $form->script("") + ->url(url::abs_file("modules/gallery/js/albums_form_add.js")); + + return $form; + } + + static function get_edit_form($parent) { + $form = new Forge( + "albums/update/{$parent->id}", "", "post", array("id" => "g-edit-album-form")); + $form->hidden("from_id")->value($parent->id); + $group = $form->group("edit_item")->label(t("Edit Album")); + + $group->input("title")->label(t("Title"))->value($parent->title) + ->error_messages("required", t("You must provide a title")) + ->error_messages("length", t("Your title is too long")); + $group->textarea("description")->label(t("Description"))->value($parent->description); + if ($parent->id != 1) { + $group->input("name")->label(t("Directory Name"))->value($parent->name) + ->error_messages("conflict", t("There is already a movie, photo or album with this name")) + ->error_messages("no_slashes", t("The directory name can't contain a \"/\"")) + ->error_messages("no_trailing_period", t("The directory name can't end in \".\"")) + ->error_messages("required", t("You must provide a directory name")) + ->error_messages("length", t("Your directory name is too long")); + $group->input("slug")->label(t("Internet Address"))->value($parent->slug) + ->error_messages( + "conflict", t("There is already a movie, photo or album with this internet address")) + ->error_messages( + "reserved", t("This address is reserved and can't be used.")) + ->error_messages( + "not_url_safe", + t("The internet address should contain only letters, numbers, hyphens and underscores")) + ->error_messages("required", t("You must provide an internet address")) + ->error_messages("length", t("Your internet address is too long")); + } else { + $group->hidden("name")->value($parent->name); + $group->hidden("slug")->value($parent->slug); + } + + $sort_order = $group->group("sort_order", array("id" => "g-album-sort-order")) + ->label(t("Sort Order")); + + $sort_order->dropdown("column", array("id" => "g-album-sort-column")) + ->label(t("Sort by")) + ->options(album::get_sort_order_options()) + ->selected($parent->sort_column); + $sort_order->dropdown("direction", array("id" => "g-album-sort-direction")) + ->label(t("Order")) + ->options(array("ASC" => t("Ascending"), + "DESC" => t("Descending"))) + ->selected($parent->sort_order); + + module::event("item_edit_form", $parent, $form); + + $group = $form->group("buttons")->label(""); + $group->hidden("type")->value("album"); + $group->submit("")->value(t("Modify")); + return $form; + } + + /** + * Return a structured set of all the possible sort orders. + */ + static function get_sort_order_options() { + return array("weight" => t("Manual"), + "captured" => t("Date captured"), + "created" => t("Date uploaded"), + "title" => t("Title"), + "name" => t("File name"), + "updated" => t("Date modified"), + "view_count" => t("Number of views"), + "rand_key" => t("Random")); + } +} diff --git a/modules/gallery/helpers/auth.php b/modules/gallery/helpers/auth.php new file mode 100644 index 0000000..2eb3c25 --- /dev/null +++ b/modules/gallery/helpers/auth.php @@ -0,0 +1,134 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class 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->hidden("continue_url")->value(Session::instance()->get("continue_url")); + $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") + ->error_messages( + "too_many_failed_logins", t("Too many failed login attempts. Try again later")); + $group->password("password")->label(t("Password"))->id("g-password")->class(null); + $group->inputs["name"]->error_messages("invalid_login", t("Invalid name or password")); + $group->submit("")->value(t("Login")); + return $form; + } + + static function login($user) { + identity::set_active_user($user); + if (identity::is_writable()) { + $user->login_count += 1; + $user->last_login = time(); + $user->save(); + } + log::info("user", t("User %name logged in", array("name" => $user->name))); + module::event("user_login", $user); + } + + static function logout() { + $user = identity::active_user(); + if (!$user->guest) { + try { + Session::instance()->destroy(); + } catch (Exception $e) { + Kohana_Log::add("error", $e); + } + module::event("user_logout", $user); + } + log::info("user", t("User %name logged out", array("name" => $user->name)), + t('<a href="%url">%user_name</a>', + array("url" => user_profile::url($user->id), + "user_name" => html::clean($user->name)))); + } + + /** + * After there have been 5 failed auth attempts, any failure leads to getting locked out for a + * minute. + */ + static function too_many_failures($name) { + $failed = ORM::factory("failed_auth") + ->where("name", "=", $name) + ->find(); + return ($failed->loaded() && + $failed->count > 5 && + (time() - $failed->time < 60)); + } + + static function validate_too_many_failed_logins($name_input) { + if (auth::too_many_failures($name_input->value)) { + $name_input->add_error("too_many_failed_logins", 1); + } + } + + static function validate_too_many_failed_auth_attempts($form_input) { + if (auth::too_many_failures(identity::active_user()->name)) { + $form_input->add_error("too_many_failed_auth_attempts", 1); + } + } + + /** + * Record a failed authentication for this user + */ + static function record_failed_attempt($name) { + $failed = ORM::factory("failed_auth") + ->where("name", "=", $name) + ->find(); + if (!$failed->loaded()) { + $failed->name = $name; + } + $failed->time = time(); + $failed->count++; + $failed->save(); + } + + /** + * Clear any failed logins for this user + */ + static function clear_failed_attempts($user) { + ORM::factory("failed_auth") + ->where("name", "=", $user->name) + ->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; + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/batch.php b/modules/gallery/helpers/batch.php new file mode 100644 index 0000000..bf2425e --- /dev/null +++ b/modules/gallery/helpers/batch.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class batch_Core { + static function start() { + $session = Session::instance(); + $session->set("batch_level", $session->get("batch_level", 0) + 1); + } + + static function stop() { + $session = Session::instance(); + $batch_level = $session->get("batch_level", 0) - 1; + if ($batch_level > 0) { + $session->set("batch_level", $batch_level); + } else { + $session->delete("batch_level"); + module::event("batch_complete"); + } + } + + static function in_progress() { + return Session::instance()->get("batch_level", 0) > 0; + } +} diff --git a/modules/gallery/helpers/block_manager.php b/modules/gallery/helpers/block_manager.php new file mode 100644 index 0000000..a227946 --- /dev/null +++ b/modules/gallery/helpers/block_manager.php @@ -0,0 +1,115 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class block_manager_Core { + static function get_active($location) { + return unserialize(module::get_var("gallery", "blocks_$location", "a:0:{}")); + } + + static function set_active($location, $blocks) { + module::set_var("gallery", "blocks_$location", serialize($blocks)); + } + + static function add($location, $module_name, $block_id) { + $blocks = block_manager::get_active($location); + $blocks[random::int()] = array($module_name, $block_id); + + block_manager::set_active($location, $blocks); + } + + static function activate_blocks($module_name) { + $block_class = "{$module_name}_block"; + if (class_exists($block_class) && method_exists($block_class, "get_site_list")) { + $blocks = call_user_func(array($block_class, "get_site_list")); + foreach (array_keys($blocks) as $block_id) { + block_manager::add("site_sidebar", $module_name, $block_id); + } + } + } + + static function remove($location, $block_id) { + $blocks = block_manager::get_active($location); + unset($blocks[$block_id]); + block_manager::set_active($location, $blocks); + } + + static function remove_blocks_for_module($location, $module_name) { + $blocks = block_manager::get_active($location); + foreach ($blocks as $key => $block) { + if ($block[0] == $module_name) { + unset($blocks[$key]); + } + } + block_manager::set_active($location, $blocks); + } + + static function deactivate_blocks($module_name) { + $block_class = "{$module_name}_block"; + if (class_exists($block_class) && method_exists($block_class, "get_site_list")) { + $blocks = call_user_func(array($block_class, "get_site_list")); + foreach (array_keys($blocks) as $block_id) { + block_manager::remove_blocks_for_module("site_sidebar", $module_name); + } + } + + if (class_exists($block_class) && method_exists($block_class, "get_admin_list")) { + $blocks = call_user_func(array($block_class, "get_admin_list")); + foreach (array("dashboard_sidebar", "dashboard_center") as $location) { + block_manager::remove_blocks_for_module($location, $module_name); + } + } + } + + static function get_available_admin_blocks() { + return self::_get_blocks("get_admin_list"); + } + + static function get_available_site_blocks() { + return self::_get_blocks("get_site_list"); + } + + private static function _get_blocks($function) { + $blocks = array(); + + foreach (module::active() as $module) { + $class_name = "{$module->name}_block"; + if (class_exists($class_name) && method_exists($class_name, $function)) { + foreach (call_user_func(array($class_name, $function)) as $id => $title) { + $blocks["{$module->name}:$id"] = $title; + } + } + } + return $blocks; + } + + static function get_html($location, $theme=null) { + $active = block_manager::get_active($location); + $result = ""; + foreach ($active as $id => $desc) { + if (class_exists("$desc[0]_block") && method_exists("$desc[0]_block", "get")) { + $block = call_user_func(array("$desc[0]_block", "get"), $desc[1], $theme); + if (!empty($block)) { + $block->id = $id; + $result .= $block; + } + } + } + return $result; + } +} diff --git a/modules/gallery/helpers/data_rest.php b/modules/gallery/helpers/data_rest.php new file mode 100644 index 0000000..a0a225f --- /dev/null +++ b/modules/gallery/helpers/data_rest.php @@ -0,0 +1,115 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This resource returns the raw contents of Item_Model data files. It's analogous to the + * file_proxy controller, but it uses the REST authentication model. + */ +class data_rest_Core { + static function get($request) { + $item = rest::resolve($request->url); + + $p = $request->params; + if (!isset($p->size) || !in_array($p->size, array("thumb", "resize", "full"))) { + throw new Rest_Exception("Bad Request", 400, array("errors" => array("size" => "invalid"))); + } + + // Note: this code is roughly duplicated in file_proxy, so if you modify this, please look to + // see if you should make the same change there as well. + + if ($p->size == "full") { + if ($item->is_album()) { + throw new Kohana_404_Exception(); + } + access::required("view_full", $item); + $file = $item->file_path(); + } else if ($p->size == "resize") { + access::required("view", $item); + $file = $item->resize_path(); + } else { + access::required("view", $item); + $file = $item->thumb_path(); + } + + if (!file_exists($file)) { + throw new Kohana_404_Exception(); + } + + header("Content-Length: " . filesize($file)); + + if (isset($p->m)) { + header("Pragma:"); + // Check that the content hasn't expired or it wasn't changed since cached + expires::check(2592000, $item->updated); + + expires::set(2592000, $item->updated); // 30 days + } + + // We don't need to save the session for this request + Session::instance()->abort_save(); + + // Dump out the image. If the item is a movie or album, then its thumbnail will be a JPG. + if (($item->is_movie() || $item->is_album()) && $p->size == "thumb") { + header("Content-Type: image/jpeg"); + } else { + header("Content-Type: $item->mime_type"); + } + + if (TEST_MODE) { + return $file; + } else { + Kohana::close_buffers(false); + + if (isset($p->encoding) && $p->encoding == "base64") { + print base64_encode(file_get_contents($file)); + } else { + readfile($file); + } + } + + // We must exit here to keep the regular REST framework reply code from adding more bytes on + // at the end or tinkering with headers. + exit; + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + return $item; + } + + static function url($item, $size) { + if ($size == "full") { + $file = $item->file_path(); + } else if ($size == "resize") { + $file = $item->resize_path(); + } else { + $file = $item->thumb_path(); + } + if (!file_exists($file)) { + throw new Kohana_404_Exception(); + } + + return url::abs_site("rest/data/{$item->id}?size=$size&m=" . filemtime($file)); + } +} + diff --git a/modules/gallery/helpers/dir.php b/modules/gallery/helpers/dir.php new file mode 100644 index 0000000..807f7bd --- /dev/null +++ b/modules/gallery/helpers/dir.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class dir_Core { + static function unlink($path) { + if (is_dir($path) && is_writable($path)) { + foreach (new DirectoryIterator($path) as $resource) { + if ($resource->isDot()) { + unset($resource); + continue; + } else if ($resource->isFile()) { + unlink($resource->getPathName()); + } else if ($resource->isDir()) { + dir::unlink($resource->getRealPath()); + } + unset($resource); + } + return @rmdir($path); + } + return false; + } + + +} diff --git a/modules/gallery/helpers/encoding.php b/modules/gallery/helpers/encoding.php new file mode 100644 index 0000000..073aef9 --- /dev/null +++ b/modules/gallery/helpers/encoding.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class encoding_Core { + static function convert_to_utf8($value) { + if (function_exists("mb_detect_encoding")) { + // Rely on mb_detect_encoding()'s strict mode + $src_encoding = mb_detect_encoding($value, mb_detect_order(), true); + if ($src_encoding != "UTF-8") { + if (function_exists("mb_convert_encoding") && $src_encoding) { + $value = mb_convert_encoding($value, "UTF-8", $src_encoding); + } else { + $value = utf8_encode($value); + } + } + } + return $value; + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery.php b/modules/gallery/helpers/gallery.php new file mode 100644 index 0000000..0adabe4 --- /dev/null +++ b/modules/gallery/helpers/gallery.php @@ -0,0 +1,233 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_Core { + const VERSION = "3.0.9"; + const CODE_NAME = "Chartres"; + const RELEASE_CHANNEL = "release"; + const RELEASE_BRANCH = "3.0.x"; + + /** + * If Gallery is in maintenance mode, then force all non-admins to get routed to a "This site is + * down for maintenance" page. + */ + static function maintenance_mode() { + if (module::get_var("gallery", "maintenance_mode", 0) && + !identity::active_user()->admin) { + try { + $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller'); + $allowed = $class->getConstant("ALLOW_MAINTENANCE_MODE") === true; + } catch (ReflectionClass $e) { + $allowed = false; + } + if (!$allowed) { + if (Router::$controller == "admin") { + // At this point we're in the admin theme and it doesn't have a themed login page, so + // we can't just swap in the login controller and have it work. So redirect back to the + // root item where we'll run this code again with the site theme. + url::redirect(item::root()->abs_url()); + } else { + Session::instance()->set("continue_url", url::abs_site("admin/maintenance")); + Router::$controller = "login"; + Router::$controller_path = MODPATH . "gallery/controllers/login.php"; + Router::$method = "html"; + } + } + } + } + + /** + * If the gallery is only available to registered users and the user is not logged in, present + * the login page. + */ + static function private_gallery() { + if (identity::active_user()->guest && + !access::user_can(identity::guest(), "view", item::root()) && + php_sapi_name() != "cli") { + try { + $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller'); + $allowed = $class->getConstant("ALLOW_PRIVATE_GALLERY") === true; + } catch (ReflectionClass $e) { + $allowed = false; + } + if (!$allowed) { + if (Router::$controller == "admin") { + // At this point we're in the admin theme and it doesn't have a themed login page, so + // we can't just swap in the login controller and have it work. So redirect back to the + // root item where we'll run this code again with the site theme. + url::redirect(item::root()->abs_url()); + } else { + Session::instance()->set("continue_url", url::abs_current()); + Router::$controller = "login"; + Router::$controller_path = MODPATH . "gallery/controllers/login.php"; + Router::$method = "html"; + } + } + } + } + + /** + * This function is called when the Gallery is fully initialized. We relay it to modules as the + * "gallery_ready" event. Any module that wants to perform an action at the start of every + * request should implement the <module>_event::gallery_ready() handler. + */ + static function ready() { + // Don't keep a session for robots; it's a waste of database space. + if (request::user_agent("robot")) { + Session::instance()->abort_save(); + } + + module::event("gallery_ready"); + } + + /** + * This function is called right before the Kohana framework shuts down. We relay it to modules + * as the "gallery_shutdown" event. Any module that wants to perform an action at the start of + * every request should implement the <module>_event::gallery_shutdown() handler. + */ + static function shutdown() { + module::event("gallery_shutdown"); + } + + /** + * Return a unix timestamp in a user specified format including date and time. + * @param $timestamp unix timestamp + * @return string + */ + static function date_time($timestamp) { + return date(module::get_var("gallery", "date_time_format"), $timestamp); + } + + /** + * Return a unix timestamp in a user specified format that's just the date. + * @param $timestamp unix timestamp + * @return string + */ + static function date($timestamp) { + return date(module::get_var("gallery", "date_format"), $timestamp); + } + + /** + * Return a unix timestamp in a user specified format that's just the time. + * @param $timestamp unix timestamp + * @return string + */ + static function time($timestamp) { + return date(module::get_var("gallery", "time_format", "H:i:s"), $timestamp); + } + + /** + * Provide a wrapper function for Kohana::find_file that first strips the extension and + * then calls the Kohana::find_file and supplies the extension as the type. + * @param string directory to search in + * @param string filename to look for + * @param boolean file required (optional: default false) + * @return array if the extension is config, i18n or l10n + * @return string if the file is found (relative to the DOCROOT) + * @return false if the file is not found + */ + static function find_file($directory, $file, $required=false) { + $file_name = substr($file, 0, -strlen($ext = strrchr($file, '.'))); + $file_name = Kohana::find_file($directory, $file_name, $required, substr($ext, 1)); + if (!$file_name) { + if (file_exists(DOCROOT . "lib/$directory/$file")) { + return "lib/$directory/$file"; + } else if (file_exists(DOCROOT . "lib/$file")) { + return "lib/$file"; + } + } + + if (is_string($file_name)) { + // make relative to DOCROOT + $parts = explode("/", $file_name); + $count = count($parts); + foreach ($parts as $idx => $part) { + // If this part is "modules" or "themes" make sure that the part 2 after this + // is the target directory, and if it is then we're done. This check makes + // sure that if Gallery is installed in a directory called "modules" or "themes" + // We don't parse the directory structure incorrectly. + if (in_array($part, array("modules", "themes")) && + $idx + 2 < $count && + $parts[$idx + 2] == $directory) { + break; + } + unset($parts[$idx]); + } + $file_name = implode("/", $parts); + } + return $file_name; + } + + /** + * Set the PATH environment variable to the paths specified. + * @param array Array of paths. Each array entry can contain a colon separated list of paths. + */ + static function set_path_env($paths) { + $path_env = array(); + foreach ($paths as $path) { + if ($path) { + array_push($path_env, $path); + } + } + putenv("PATH=" . implode(":", $path_env)); + } + + /** + * Return a string describing this version of Gallery and the type of release. + */ + static function version_string() { + if (gallery::RELEASE_CHANNEL == "git") { + $build_number = gallery::build_number(); + return sprintf( + "%s (branch %s, %s)", gallery::VERSION, gallery::RELEASE_BRANCH, + $build_number ? " build $build_number" : "unknown build number"); + } else { + return sprintf("%s (%s)", gallery::VERSION, gallery::CODE_NAME); + } + } + + /** + * Return the contents of the .build_number file, which should be a single integer + * or return null if the .build_number file is missing. + */ + static function build_number() { + $build_file = DOCROOT . ".build_number"; + if (file_exists($build_file)) { + $result = parse_ini_file(DOCROOT . ".build_number"); + return $result["build_number"]; + } + return null; + } + + /** + * Return true if we should show the profiler at the bottom of the page. Note that this + * function is called at database setup time so it cannot rely on the database. + */ + static function show_profiler() { + return file_exists(VARPATH . "PROFILE"); + } + + /** + * Return true if we should allow Javascript and CSS combining for performance reasons. + * Typically we want this, but it's convenient for developers to be able to disable it. + */ + static function allow_css_and_js_combining() { + return !file_exists(VARPATH . "DONT_COMBINE"); + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery_block.php b/modules/gallery/helpers/gallery_block.php new file mode 100644 index 0000000..5ac4d74 --- /dev/null +++ b/modules/gallery/helpers/gallery_block.php @@ -0,0 +1,145 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_block_Core { + static function get_admin_list() { + return array( + "welcome" => t("Welcome to Gallery 3!"), + "photo_stream" => t("Photo stream"), + "log_entries" => t("Log entries"), + "stats" => t("Gallery stats"), + "platform_info" => t("Platform information"), + "project_news" => t("Gallery project news"), + "upgrade_checker" => t("Check for Gallery upgrades") + ); + } + + static function get_site_list() { + return array("language" => t("Language preference")); + } + + static function get($block_id) { + $block = new Block(); + switch ($block_id) { + case "welcome": + $block->css_id = "g-welcome"; + $block->title = t("Welcome to Gallery 3"); + $block->content = new View("admin_block_welcome.html"); + break; + + case "photo_stream": + $block->css_id = "g-photo-stream"; + $block->title = t("Photo stream"); + $block->content = new View("admin_block_photo_stream.html"); + $block->content->photos = ORM::factory("item") + ->where("type", "=", "photo")->order_by("created", "DESC")->find_all(10); + break; + + case "log_entries": + $block->css_id = "g-log-entries"; + $block->title = t("Log entries"); + $block->content = new View("admin_block_log_entries.html"); + $block->content->entries = ORM::factory("log") + ->order_by(array("timestamp" => "DESC", "id" => "DESC"))->find_all(5); + break; + + case "stats": + $block->css_id = "g-stats"; + $block->title = t("Gallery stats"); + $block->content = new View("admin_block_stats.html"); + $block->content->album_count = + ORM::factory("item")->where("type", "=", "album")->where("id", "<>", 1)->count_all(); + $block->content->photo_count = ORM::factory("item")->where("type", "=", "photo")->count_all(); + break; + + case "platform_info": + $block->css_id = "g-platform"; + $block->title = t("Platform information"); + $block->content = new View("admin_block_platform.html"); + break; + + case "project_news": + $block->css_id = "g-project-news"; + $block->title = t("Gallery project news"); + $block->content = new View("admin_block_news.html"); + $block->content->feed = feed::parse("http://galleryproject.org/node/feed", 3); + break; + + case "block_adder": + if ($form = gallery_block::get_add_block_form()) { + $block->css_id = "g-block-adder"; + $block->title = t("Dashboard content"); + $block->content = $form; + } else { + $block = ""; + } + break; + + case "language": + $locales = locales::installed(); + if (count($locales) > 1) { + foreach ($locales as $locale => $display_name) { + $locales[$locale] = SafeString::of_safe_html($display_name); + } + $block = new Block(); + $block->css_id = "g-user-language-block"; + $block->title = t("Language preference"); + $block->content = new View("user_languages_block.html"); + $block->content->installed_locales = array_merge(array("" => t("« none »")), $locales); + $block->content->selected = (string) locales::cookie_locale(); + } else { + $block = ""; + } + break; + + case "upgrade_checker": + $block = new Block(); + $block->css_id = "g-upgrade-available-block"; + $block->title = t("Check for Gallery upgrades"); + $block->content = new View("upgrade_checker_block.html"); + $block->content->version_info = upgrade_checker::version_info(); + $block->content->auto_check_enabled = upgrade_checker::auto_check_enabled(); + $block->content->new_version = upgrade_checker::get_upgrade_message(); + $block->content->build_number = gallery::build_number(); + } + return $block; + } + + static function get_add_block_form() { + $available_blocks = block_manager::get_available_admin_blocks(); + + $active = array(); + foreach (array_merge(block_manager::get_active("dashboard_sidebar"), + block_manager::get_active("dashboard_center")) as $b) { + unset($available_blocks[implode(":", $b)]); + } + + if (!$available_blocks) { + return; + } + + $form = new Forge("admin/dashboard/add_block", "", "post", + array("id" => "g-add-dashboard-block-form")); + $group = $form->group("add_block")->label(t("Add Block")); + $group->dropdown("id")->label(t("Available blocks"))->options($available_blocks); + $group->submit("center")->value(t("Add to center")); + $group->submit("sidebar")->value(t("Add to sidebar")); + return $form; + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery_error.php b/modules/gallery/helpers/gallery_error.php new file mode 100644 index 0000000..76c8ca9 --- /dev/null +++ b/modules/gallery/helpers/gallery_error.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_error_Core { + static function error_handler($severity, $message, $filename, $lineno) { + if (error_reporting() == 0) { + return; + } + + if (error_reporting() & $severity) { + throw new ErrorException($message, 0, $severity, $filename, $lineno); + } + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery_event.php b/modules/gallery/helpers/gallery_event.php new file mode 100644 index 0000000..a319b9c --- /dev/null +++ b/modules/gallery/helpers/gallery_event.php @@ -0,0 +1,621 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class gallery_event_Core { + /** + * Initialization. + */ + static function gallery_ready() { + if (!get_cfg_var("date.timezone")) { + if (!(rand() % 4)) { + Kohana_Log::add("error", "date.timezone setting not detected in " . + get_cfg_var("cfg_file_path") . " falling back to UTC. " . + "Consult http://php.net/manual/function.get-cfg-var.php for help."); + } + } + + identity::load_user(); + theme::load_themes(); + locales::set_request_locale(); + } + + static function gallery_shutdown() { + // Every 500th request, do a pass over var/logs and var/tmp and delete old files. + // Limit ourselves to deleting a single file so that we don't spend too much CPU + // time on it. As long as servers call this at least twice a day they'll eventually + // wind up with a clean var/logs directory because we only create 1 file a day there. + // var/tmp might be stickier because theoretically we could wind up spamming that + // dir with a lot of files. But let's start with this and refine as we go. + if (!(rand() % 500)) { + // Note that this code is roughly duplicated in gallery_task::file_cleanup + $threshold = time() - 1209600; // older than 2 weeks + foreach(array("logs", "tmp") as $dir) { + $dir = VARPATH . $dir; + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false) { + if ($file[0] == ".") { + continue; + } + + // Ignore directories for now, but we should really address them in the long term. + if (is_dir("$dir/$file")) { + continue; + } + + if (filemtime("$dir/$file") <= $threshold) { + unlink("$dir/$file"); + break; + } + } + } + } + } + // Delete all files marked using system::delete_later. + system::delete_marked_files(); + } + + static function user_deleted($user) { + $admin = identity::admin_user(); + if (!empty($admin)) { // could be empty if there is not identity provider + db::build() + ->update("tasks") + ->set("owner_id", $admin->id) + ->where("owner_id", "=", $user->id) + ->execute(); + db::build() + ->update("items") + ->set("owner_id", $admin->id) + ->where("owner_id", "=", $user->id) + ->execute(); + db::build() + ->update("logs") + ->set("user_id", $admin->id) + ->where("user_id", "=", $user->id) + ->execute(); + } + } + + static function identity_provider_changed($old_provider, $new_provider) { + $admin = identity::admin_user(); + db::build() + ->update("tasks") + ->set("owner_id", $admin->id) + ->execute(); + db::build() + ->update("items") + ->set("owner_id", $admin->id) + ->execute(); + db::build() + ->update("logs") + ->set("user_id", $admin->id) + ->execute(); + module::set_var("gallery", "email_from", $admin->email); + module::set_var("gallery", "email_reply_to", $admin->email); + } + + static function group_created($group) { + access::add_group($group); + } + + static function group_deleted($group) { + access::delete_group($group); + } + + static function item_created($item) { + access::add_item($item); + + // Build our thumbnail/resizes. + try { + graphics::generate($item); + } catch (Exception $e) { + log::error("graphics", t("Couldn't create a thumbnail or resize for %item_title", + array("item_title" => $item->title)), + html::anchor($item->abs_url(), t("details"))); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + + if ($item->is_photo() || $item->is_movie()) { + // If the parent has no cover item, make this it. + $parent = $item->parent(); + if (access::can("edit", $parent) && $parent->album_cover_item_id == null) { + item::make_album_cover($item); + } + } + } + + static function item_deleted($item) { + access::delete_item($item); + + // Find any other albums that had the deleted item as the album cover and null it out. + // In some cases this may leave us with a missing album cover up in this item's parent + // hierarchy, but in most cases it'll work out fine. + foreach (ORM::factory("item") + ->where("album_cover_item_id", "=", $item->id) + ->find_all() as $parent) { + item::remove_album_cover($parent); + } + + $parent = $item->parent(); + if (!$parent->album_cover_item_id) { + // Assume that we deleted the album cover + if (batch::in_progress()) { + // Remember that this parent is missing an album cover, for later. + $batch_missing_album_cover = Session::instance()->get("batch_missing_album_cover", array()); + $batch_missing_album_cover[$parent->id] = 1; + Session::instance()->set("batch_missing_album_cover", $batch_missing_album_cover); + } else { + // Choose the first viewable child as the new cover. + if ($child = $parent->viewable()->children(1)->current()) { + item::make_album_cover($child); + } + } + } + } + + static function item_updated_data_file($item) { + graphics::generate($item); + + // Update any places where this is the album cover + foreach (ORM::factory("item") + ->where("album_cover_item_id", "=", $item->id) + ->find_all() as $target) { + $target->thumb_dirty = 1; + $target->save(); + graphics::generate($target); + } + } + + static function batch_complete() { + // Set the album covers for any items that where we probably deleted the album cover during + // this batch. The item may have been deleted, so don't count on it being around. Choose the + // first child as the new album cover. + // NOTE: if the first child doesn't have an album cover, then this won't work. + foreach (array_keys(Session::instance()->get("batch_missing_album_cover", array())) as $id) { + $item = ORM::factory("item", $id); + if ($item->loaded() && !$item->album_cover_item_id) { + if ($child = $item->children(1)->current()) { + item::make_album_cover($child); + } + } + } + Session::instance()->delete("batch_missing_album_cover"); + } + + static function item_moved($item, $old_parent) { + if ($item->is_album()) { + access::recalculate_album_permissions($item->parent()); + } else { + access::recalculate_photo_permissions($item); + } + + // If the new parent doesn't have an album cover, make this it. + if (!$item->parent()->album_cover_item_id) { + item::make_album_cover($item); + } + } + + static function user_login($user) { + // If this user is an admin, check to see if there are any post-install tasks that we need + // to run and take care of those now. + if ($user->admin && module::get_var("gallery", "choose_default_tookit", null)) { + graphics::choose_default_toolkit(); + module::clear_var("gallery", "choose_default_tookit"); + } + Session::instance()->set("active_auth_timestamp", time()); + auth::clear_failed_attempts($user); + } + + static function user_auth_failed($name) { + auth::record_failed_attempt($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) { + $data[] = $item->description; + $data[] = $item->name; + $data[] = $item->title; + } + + static function user_menu($menu, $theme) { + if ($theme->page_subtype != "login") { + $user = identity::active_user(); + if ($user->guest) { + $menu->append(Menu::factory("dialog") + ->id("user_menu_login") + ->css_id("g-login-link") + ->url(url::site("login/ajax")) + ->label(t("Login"))); + } else { + $csrf = access::csrf_token(); + $menu->append(Menu::factory("link") + ->id("user_menu_edit_profile") + ->css_id("g-user-profile-link") + ->view("login_current_user.html") + ->url(user_profile::url($user->id)) + ->label($user->display_name())); + + if (Router::$controller == "admin") { + $continue_url = url::abs_site(""); + } else if ($item = $theme->item()) { + if (access::user_can(identity::guest(), "view", $theme->item)) { + $continue_url = $item->abs_url(); + } else { + $continue_url = item::root()->abs_url(); + } + } else { + $continue_url = url::abs_current(); + } + + $menu->append(Menu::factory("link") + ->id("user_menu_logout") + ->css_id("g-logout-link") + ->url(url::site("logout?csrf=$csrf&continue_url=" . urlencode($continue_url))) + ->label(t("Logout"))); + } + } + } + + static function site_menu($menu, $theme, $item_css_selector) { + if ($theme->page_subtype != "login") { + $menu->append(Menu::factory("link") + ->id("home") + ->label(t("Home")) + ->url(item::root()->url())); + + + $item = $theme->item(); + + if (!empty($item)) { + $can_edit = $item && access::can("edit", $item); + $can_add = $item && access::can("add", $item); + + if ($can_add) { + $menu->append($add_menu = Menu::factory("submenu") + ->id("add_menu") + ->label(t("Add"))); + $is_album_writable = + is_writable($item->is_album() ? $item->file_path() : $item->parent()->file_path()); + if ($is_album_writable) { + $add_menu->append(Menu::factory("dialog") + ->id("add_photos_item") + ->label(t("Add photos")) + ->url(url::site("uploader/index/$item->id"))); + if ($item->is_album()) { + $add_menu->append(Menu::factory("dialog") + ->id("add_album_item") + ->label(t("Add an album")) + ->url(url::site("form/add/albums/$item->id?type=album"))); + } + } else { + message::warning(t("The album '%album_name' is not writable.", + array("album_name" => $item->title))); + } + } + + switch ($item->type) { + case "album": + $option_text = t("Album options"); + $edit_text = t("Edit album"); + $delete_text = t("Delete album"); + break; + case "movie": + $option_text = t("Movie options"); + $edit_text = t("Edit movie"); + $delete_text = t("Delete movie"); + break; + default: + $option_text = t("Photo options"); + $edit_text = t("Edit photo"); + $delete_text = t("Delete photo"); + } + + $menu->append($options_menu = Menu::factory("submenu") + ->id("options_menu") + ->label($option_text)); + if ($item && ($can_edit || $can_add)) { + if ($can_edit) { + $options_menu->append(Menu::factory("dialog") + ->id("edit_item") + ->label($edit_text) + ->url(url::site("form/edit/{$item->type}s/$item->id?from_id={$item->id}"))); + } + + if ($item->is_album()) { + if ($can_edit) { + $options_menu->append(Menu::factory("dialog") + ->id("edit_permissions") + ->label(t("Edit permissions")) + ->url(url::site("permissions/browse/$item->id"))); + } + } + } + + $csrf = access::csrf_token(); + $page_type = $theme->page_type(); + if ($can_edit && $item->is_photo() && graphics::can("rotate")) { + $options_menu + ->append( + Menu::factory("ajax_link") + ->id("rotate_ccw") + ->label(t("Rotate 90° counter clockwise")) + ->css_class("ui-icon-rotate-ccw") + ->ajax_handler("function(data) { " . + "\$.gallery_replace_image(data, \$('$item_css_selector')) }") + ->url(url::site("quick/rotate/$item->id/ccw?csrf=$csrf&from_id={$item->id}&page_type=$page_type"))) + ->append( + Menu::factory("ajax_link") + ->id("rotate_cw") + ->label(t("Rotate 90° clockwise")) + ->css_class("ui-icon-rotate-cw") + ->ajax_handler("function(data) { " . + "\$.gallery_replace_image(data, \$('$item_css_selector')) }") + ->url(url::site("quick/rotate/$item->id/cw?csrf=$csrf&from_id={$item->id}&page_type=$page_type"))); + } + + if ($item->id != item::root()->id) { + $parent = $item->parent(); + if (access::can("edit", $parent)) { + // We can't make this item the highlight if it's an album with no album cover, or if it's + // already the album cover. + if (($item->type == "album" && empty($item->album_cover_item_id)) || + ($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) || + $parent->album_cover_item_id == $item->id) { + $disabledState = "ui-state-disabled"; + } else { + $disabledState = ""; + } + + if ($item->parent()->id != 1) { + $options_menu + ->append( + Menu::factory("ajax_link") + ->id("make_album_cover") + ->label(t("Choose as the album cover")) + ->css_class("ui-icon-star $disabledState") + ->ajax_handler("function(data) { window.location.reload() }") + ->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf"))); + } + $options_menu + ->append( + Menu::factory("dialog") + ->id("delete") + ->label($delete_text) + ->css_class("ui-icon-trash") + ->css_class("g-quick-delete") + ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&from_id={$item->id}&page_type=$page_type"))); + } + } + } + + if (identity::active_user()->admin) { + $menu->append($admin_menu = Menu::factory("submenu") + ->id("admin_menu") + ->label(t("Admin"))); + module::event("admin_menu", $admin_menu, $theme); + + $settings_menu = $admin_menu->get("settings_menu"); + uasort($settings_menu->elements, array("Menu", "title_comparator")); + } + } + } + + static function admin_menu($menu, $theme) { + $menu + ->append(Menu::factory("link") + ->id("dashboard") + ->label(t("Dashboard")) + ->url(url::site("admin"))) + ->append(Menu::factory("submenu") + ->id("settings_menu") + ->label(t("Settings")) + ->append(Menu::factory("link") + ->id("graphics_toolkits") + ->label(t("Graphics")) + ->url(url::site("admin/graphics"))) + ->append(Menu::factory("link") + ->id("movies_settings") + ->label(t("Movies")) + ->url(url::site("admin/movies"))) + ->append(Menu::factory("link") + ->id("languages") + ->label(t("Languages")) + ->url(url::site("admin/languages"))) + ->append(Menu::factory("link") + ->id("advanced") + ->label(t("Advanced")) + ->url(url::site("admin/advanced_settings")))) + ->append(Menu::factory("link") + ->id("modules") + ->label(t("Modules")) + ->url(url::site("admin/modules"))) + ->append(Menu::factory("submenu") + ->id("content_menu") + ->label(t("Content"))) + ->append(Menu::factory("submenu") + ->id("appearance_menu") + ->label(t("Appearance")) + ->append(Menu::factory("link") + ->id("themes") + ->label(t("Theme choice")) + ->url(url::site("admin/themes"))) + ->append(Menu::factory("link") + ->id("theme_options") + ->label(t("Theme options")) + ->url(url::site("admin/theme_options"))) + ->append(Menu::factory("link") + ->id("sidebar") + ->label(t("Manage sidebar")) + ->url(url::site("admin/sidebar")))) + ->append(Menu::factory("submenu") + ->id("statistics_menu") + ->label(t("Statistics"))) + ->append(Menu::factory("link") + ->id("maintenance") + ->label(t("Maintenance")) + ->url(url::site("admin/maintenance"))); + return $menu; + } + + static function context_menu($menu, $theme, $item, $thumb_css_selector) { + $menu->append($options_menu = Menu::factory("submenu") + ->id("options_menu") + ->label(t("Options")) + ->css_class("ui-icon-carat-1-n")); + + $page_type = $theme->page_type(); + if (access::can("edit", $item)) { + switch ($item->type) { + case "movie": + $edit_title = t("Edit this movie"); + $delete_title = t("Delete this movie"); + break; + + case "album": + $edit_title = t("Edit this album"); + $delete_title = t("Delete this album"); + break; + + default: + $edit_title = t("Edit this photo"); + $delete_title = t("Delete this photo"); + break; + } + $cover_title = t("Choose as the album cover"); + + $csrf = access::csrf_token(); + + $theme_item = $theme->item(); + $options_menu->append(Menu::factory("dialog") + ->id("edit") + ->label($edit_title) + ->css_class("ui-icon-pencil") + ->url(url::site("quick/form_edit/$item->id?from_id={$theme_item->id}"))); + + if ($item->is_photo() && graphics::can("rotate")) { + $options_menu + ->append( + Menu::factory("ajax_link") + ->id("rotate_ccw") + ->label(t("Rotate 90° counter clockwise")) + ->css_class("ui-icon-rotate-ccw") + ->ajax_handler("function(data) { " . + "\$.gallery_replace_image(data, \$('$thumb_css_selector')) }") + ->url(url::site("quick/rotate/$item->id/ccw?csrf=$csrf&from_id={$theme_item->id}&page_type=$page_type"))) + ->append( + Menu::factory("ajax_link") + ->id("rotate_cw") + ->label(t("Rotate 90° clockwise")) + ->css_class("ui-icon-rotate-cw") + ->ajax_handler("function(data) { " . + "\$.gallery_replace_image(data, \$('$thumb_css_selector')) }") + ->url(url::site("quick/rotate/$item->id/cw?csrf=$csrf&from_id={$theme_item->id}&page_type=$page_type"))); + } + + $parent = $item->parent(); + if (access::can("edit", $parent)) { + // We can't make this item the highlight if it's an album with no album cover, or if it's + // already the album cover. + if (($item->type == "album" && empty($item->album_cover_item_id)) || + ($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) || + $parent->album_cover_item_id == $item->id) { + $disabledState = "ui-state-disabled"; + } else { + $disabledState = ""; + } + if ($item->parent()->id != 1) { + $options_menu + ->append(Menu::factory("ajax_link") + ->id("make_album_cover") + ->label($cover_title) + ->css_class("ui-icon-star $disabledState") + ->ajax_handler("function(data) { window.location.reload() }") + ->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf"))); + } + $options_menu + ->append(Menu::factory("dialog") + ->id("delete") + ->label($delete_title) + ->css_class("ui-icon-trash") + ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&" . + "from_id={$theme_item->id}&page_type=$page_type"))); + } + + if ($item->is_album()) { + $options_menu + ->append(Menu::factory("dialog") + ->id("add_item") + ->label(t("Add a photo")) + ->css_class("ui-icon-plus") + ->url(url::site("uploader/index/$item->id"))) + ->append(Menu::factory("dialog") + ->id("add_album") + ->label(t("Add an album")) + ->css_class("ui-icon-note") + ->url(url::site("form/add/albums/$item->id?type=album"))) + ->append(Menu::factory("dialog") + ->id("edit_permissions") + ->label(t("Edit permissions")) + ->css_class("ui-icon-key") + ->url(url::site("permissions/browse/$item->id"))); + } + } + } + + static function show_user_profile($data) { + $v = new View("user_profile_info.html"); + + $fields = array("name" => t("Name"), "locale" => t("Language Preference"), + "email" => t("Email"), "full_name" => t("Full name"), "url" => t("Web site")); + if (!$data->user->guest) { + $fields = array("name" => t("Name"), "full_name" => t("Full name"), "url" => t("Web site")); + } + $v->user_profile_data = array(); + foreach ($fields as $field => $label) { + if (!empty($data->user->$field)) { + $value = $data->user->$field; + if ($field == "locale") { + $value = locales::display_name($value); + } else if ($field == "url") { + $value = html::mark_clean(html::anchor(html::clean($data->user->$field))); + } + $v->user_profile_data[(string) $label] = $value; + } + } + $data->content[] = (object) array("title" => t("User information"), "view" => $v); + + } + + static function user_updated($original_user, $updated_user) { + // If the default from/reply-to email address is set to the install time placeholder value + // of unknown@unknown.com then adopt the value from the first admin to set their own email + // address so that we at least have a valid address for the Gallery. + if ($updated_user->admin) { + $email = module::get_var("gallery", "email_from", ""); + if ($email == "unknown@unknown.com") { + module::set_var("gallery", "email_from", $updated_user->email); + module::set_var("gallery", "email_reply_to", $updated_user->email); + } + } + } +} diff --git a/modules/gallery/helpers/gallery_graphics.php b/modules/gallery/helpers/gallery_graphics.php new file mode 100644 index 0000000..eb76353 --- /dev/null +++ b/modules/gallery/helpers/gallery_graphics.php @@ -0,0 +1,183 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_graphics_Core { + /** + * Rotate an image. Valid options are degrees + * + * @param string $input_file + * @param string $output_file + * @param array $options + * @param Item_Model $item (optional) + */ + static function rotate($input_file, $output_file, $options, $item=null) { + graphics::init_toolkit(); + + $temp_file = system::temp_filename("rotate_", pathinfo($output_file, PATHINFO_EXTENSION)); + module::event("graphics_rotate", $input_file, $temp_file, $options, $item); + + if (@filesize($temp_file) > 0) { + // A graphics_rotate event made an image - move it to output_file and use it. + @rename($temp_file, $output_file); + } else { + // No events made an image - proceed with standard process. + if (@filesize($input_file) == 0) { + throw new Exception("@todo EMPTY_INPUT_FILE"); + } + + if (!isset($options["degrees"])) { + $options["degrees"] = 0; + } + + // Rotate the image. This also implicitly converts its format if needed. + Image::factory($input_file) + ->quality(module::get_var("gallery", "image_quality")) + ->rotate($options["degrees"]) + ->save($output_file); + } + + module::event("graphics_rotate_completed", $input_file, $output_file, $options, $item); + } + + /** + * Resize an image. Valid options are width, height and master. Master is one of the Image + * master dimension constants. + * + * @param string $input_file + * @param string $output_file + * @param array $options + * @param Item_Model $item (optional) + */ + static function resize($input_file, $output_file, $options, $item=null) { + graphics::init_toolkit(); + + $temp_file = system::temp_filename("resize_", pathinfo($output_file, PATHINFO_EXTENSION)); + module::event("graphics_resize", $input_file, $temp_file, $options, $item); + + if (@filesize($temp_file) > 0) { + // A graphics_resize event made an image - move it to output_file and use it. + @rename($temp_file, $output_file); + } else { + // No events made an image - proceed with standard process. + if (@filesize($input_file) == 0) { + throw new Exception("@todo EMPTY_INPUT_FILE"); + } + + list ($input_width, $input_height, $input_mime, $input_extension) = + photo::get_file_metadata($input_file); + if ($input_width && $input_height && + (empty($options["width"]) || empty($options["height"]) || empty($options["master"]) || + (max($input_width, $input_height) <= min($options["width"], $options["height"])))) { + // Photo dimensions well-defined, but options not well-defined or would upscale the image. + // Do not resize. Check mimes to see if we can copy the file or if we need to convert it. + // (checking mimes avoids needlessly converting jpg to jpeg, etc.) + $output_mime = legal_file::get_photo_types_by_extension(pathinfo($output_file, PATHINFO_EXTENSION)); + if ($input_mime && $output_mime && ($input_mime == $output_mime)) { + // Mimes well-defined and identical - copy input to output + copy($input_file, $output_file); + } else { + // Mimes not well-defined or not the same - convert input to output + $image = Image::factory($input_file) + ->quality(module::get_var("gallery", "image_quality")) + ->save($output_file); + } + } else { + // Resize the image. This also implicitly converts its format if needed. + $image = Image::factory($input_file) + ->resize($options["width"], $options["height"], $options["master"]) + ->quality(module::get_var("gallery", "image_quality")); + if (graphics::can("sharpen")) { + $image->sharpen(module::get_var("gallery", "image_sharpen")); + } + $image->save($output_file); + } + } + + module::event("graphics_resize_completed", $input_file, $output_file, $options, $item); + } + + /** + * Overlay an image on top of the input file. + * + * Valid options are: file, position, transparency, padding + * + * Valid positions: northwest, north, northeast, + * west, center, east, + * southwest, south, southeast + * + * padding is in pixels + * + * @param string $input_file + * @param string $output_file + * @param array $options + * @param Item_Model $item (optional) + */ + static function composite($input_file, $output_file, $options, $item=null) { + try { + graphics::init_toolkit(); + + $temp_file = system::temp_filename("composite_", pathinfo($output_file, PATHINFO_EXTENSION)); + module::event("graphics_composite", $input_file, $temp_file, $options, $item); + + if (@filesize($temp_file) > 0) { + // A graphics_composite event made an image - move it to output_file and use it. + @rename($temp_file, $output_file); + } else { + // No events made an image - proceed with standard process. + + list ($width, $height) = photo::get_file_metadata($input_file); + list ($w_width, $w_height) = photo::get_file_metadata($options["file"]); + + $pad = isset($options["padding"]) ? $options["padding"] : 10; + $top = $pad; + $left = $pad; + $y_center = max($height / 2 - $w_height / 2, $pad); + $x_center = max($width / 2 - $w_width / 2, $pad); + $bottom = max($height - $w_height - $pad, $pad); + $right = max($width - $w_width - $pad, $pad); + + switch ($options["position"]) { + case "northwest": $x = $left; $y = $top; break; + case "north": $x = $x_center; $y = $top; break; + case "northeast": $x = $right; $y = $top; break; + case "west": $x = $left; $y = $y_center; break; + case "center": $x = $x_center; $y = $y_center; break; + case "east": $x = $right; $y = $y_center; break; + case "southwest": $x = $left; $y = $bottom; break; + case "south": $x = $x_center; $y = $bottom; break; + case "southeast": $x = $right; $y = $bottom; break; + } + + Image::factory($input_file) + ->composite($options["file"], $x, $y, $options["transparency"]) + ->quality(module::get_var("gallery", "image_quality")) + ->save($output_file); + } + + module::event("graphics_composite_completed", $input_file, $output_file, $options, $item); + } catch (ErrorException $e) { + // Unlike rotate and resize, composite catches its exceptions here. This is because + // composite is typically called for watermarks. If during thumb/resize generation + // the watermark fails, we'd still like the image resized, just without its watermark. + // If the exception isn't caught here, graphics::generate will replace it with a + // placeholder. + Kohana_Log::add("error", $e->getMessage()); + } + } +} diff --git a/modules/gallery/helpers/gallery_installer.php b/modules/gallery/helpers/gallery_installer.php new file mode 100644 index 0000000..d49be83 --- /dev/null +++ b/modules/gallery/helpers/gallery_installer.php @@ -0,0 +1,844 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE {access_caches} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9), + PRIMARY KEY (`id`), + KEY (`item_id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {access_intents} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9), + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + + // Using a simple index instead of a unique key for the + // key column to avoid handling of concurrency issues + // on insert. Thus allowing concurrent inserts on the + // same cache key, as does Memcache / xcache. + $db->query("CREATE TABLE {caches} ( + `id` int(9) NOT NULL auto_increment, + `key` varchar(255) NOT NULL, + `tags` varchar(255), + `expiration` int(9) NOT NULL, + `cache` longblob, + PRIMARY KEY (`id`), + UNIQUE KEY (`key`), + KEY (`tags`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {failed_auths} ( + `id` int(9) NOT NULL auto_increment, + `count` int(9) NOT NULL, + `name` varchar(255) NOT NULL, + `time` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {graphics_rules} ( + `id` int(9) NOT NULL auto_increment, + `active` BOOLEAN default 0, + `args` varchar(255) default NULL, + `module_name` varchar(64) NOT NULL, + `operation` varchar(64) NOT NULL, + `priority` int(9) NOT NULL, + `target` varchar(32) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {incoming_translations} ( + `id` int(9) NOT NULL auto_increment, + `key` char(32) NOT NULL, + `locale` char(10) NOT NULL, + `message` text NOT NULL, + `revision` int(9) DEFAULT NULL, + `translation` text, + PRIMARY KEY (`id`), + UNIQUE KEY(`key`, `locale`), + KEY `locale_key` (`locale`, `key`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {items} ( + `id` int(9) NOT NULL auto_increment, + `album_cover_item_id` int(9) default NULL, + `captured` int(9) default NULL, + `created` int(9) default NULL, + `description` text default NULL, + `height` int(9) default NULL, + `left_ptr` int(9) NOT NULL, + `level` int(9) NOT NULL, + `mime_type` varchar(64) default NULL, + `name` varchar(255) default NULL, + `owner_id` int(9) default NULL, + `parent_id` int(9) NOT NULL, + `rand_key` decimal(11,10) default NULL, + `relative_path_cache` varchar(255) default NULL, + `relative_url_cache` varchar(255) default NULL, + `resize_dirty` boolean default 1, + `resize_height` int(9) default NULL, + `resize_width` int(9) default NULL, + `right_ptr` int(9) NOT NULL, + `slug` varchar(255) default NULL, + `sort_column` varchar(64) default NULL, + `sort_order` char(4) default 'ASC', + `thumb_dirty` boolean default 1, + `thumb_height` int(9) default NULL, + `thumb_width` int(9) default NULL, + `title` varchar(255) default NULL, + `type` varchar(32) NOT NULL, + `updated` int(9) default NULL, + `view_count` int(9) default 0, + `weight` int(9) NOT NULL default 0, + `width` int(9) default NULL, + PRIMARY KEY (`id`), + KEY `parent_id` (`parent_id`), + KEY `type` (`type`), + KEY `random` (`rand_key`), + KEY `weight` (`weight` DESC), + KEY `left_ptr` (`left_ptr`), + KEY `relative_path_cache` (`relative_path_cache`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {logs} ( + `id` int(9) NOT NULL auto_increment, + `category` varchar(64) default NULL, + `html` varchar(255) default NULL, + `message` text default NULL, + `referer` varchar(255) default NULL, + `severity` int(9) default 0, + `timestamp` int(9) default 0, + `url` varchar(255) default NULL, + `user_id` int(9) default 0, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {messages} ( + `id` int(9) NOT NULL auto_increment, + `key` varchar(255) default NULL, + `severity` varchar(32) default NULL, + `value` text default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`key`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {modules} ( + `id` int(9) NOT NULL auto_increment, + `active` BOOLEAN default 0, + `name` varchar(64) default NULL, + `version` int(9) default NULL, + `weight` int(9) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`), + KEY (`weight`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {outgoing_translations} ( + `id` int(9) NOT NULL auto_increment, + `base_revision` int(9) DEFAULT NULL, + `key` char(32) NOT NULL, + `locale` char(10) NOT NULL, + `message` text NOT NULL, + `translation` text, + PRIMARY KEY (`id`), + UNIQUE KEY(`key`, `locale`), + KEY `locale_key` (`locale`, `key`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {permissions} ( + `id` int(9) NOT NULL auto_increment, + `display_name` varchar(64) default NULL, + `name` varchar(64) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {sessions} ( + `session_id` varchar(127) NOT NULL, + `data` text NOT NULL, + `last_activity` int(10) UNSIGNED NOT NULL, + PRIMARY KEY (`session_id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {tasks} ( + `id` int(9) NOT NULL auto_increment, + `callback` varchar(128) default NULL, + `context` text NOT NULL, + `done` boolean default 0, + `name` varchar(128) default NULL, + `owner_id` int(9) default NULL, + `percent_complete` int(9) default 0, + `state` varchar(32) default NULL, + `status` varchar(255) default NULL, + `updated` int(9) default NULL, + PRIMARY KEY (`id`), + KEY (`owner_id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {themes} ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(64) default NULL, + `version` int(9) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE {vars} ( + `id` int(9) NOT NULL auto_increment, + `module_name` varchar(64) NOT NULL, + `name` varchar(64) NOT NULL, + `value` text, + PRIMARY KEY (`id`), + UNIQUE KEY(`module_name`, `name`)) + DEFAULT CHARSET=utf8;"); + + foreach (array("albums", "logs", "modules", "resizes", "thumbs", "tmp", "uploads") as $dir) { + @mkdir(VARPATH . $dir); + if (in_array($dir, array("logs", "tmp", "uploads"))) { + self::_protect_directory(VARPATH . $dir); + } + } + + access::register_permission("view", "View"); + access::register_permission("view_full", "View full size"); + access::register_permission("edit", "Edit"); + access::register_permission("add", "Add"); + + // Mark for translation (must be the same strings as used above) + t("View full size"); + t("View"); + t("Edit"); + t("Add"); + + // Hardcode the first item to sidestep ORM validation rules + $now = time(); + db::build()->insert( + "items", + array("created" => $now, + "description" => "", + "left_ptr" => 1, + "level" => 1, + "parent_id" => 0, + "resize_dirty" => 1, + "right_ptr" => 2, + "sort_column" => "weight", + "sort_order" => "ASC", + "thumb_dirty" => 1, + "title" => "Gallery", + "type" => "album", + "updated" => $now, + "weight" => 1)) + ->execute(); + $root = ORM::factory("item", 1); + access::add_item($root); + + module::set_var("gallery", "active_site_theme", "wind"); + module::set_var("gallery", "active_admin_theme", "admin_wind"); + module::set_var("gallery", "page_size", 9); + module::set_var("gallery", "thumb_size", 200); + module::set_var("gallery", "resize_size", 640); + module::set_var("gallery", "default_locale", "en_US"); + module::set_var("gallery", "image_quality", 75); + module::set_var("gallery", "image_sharpen", 15); + module::set_var("gallery", "upgrade_checker_auto_enabled", true); + + // Add rules for generating our thumbnails and resizes + graphics::add_rule( + "gallery", "thumb", "gallery_graphics::resize", + array("width" => 200, "height" => 200, "master" => Image::AUTO), + 100); + graphics::add_rule( + "gallery", "resize", "gallery_graphics::resize", + array("width" => 640, "height" => 640, "master" => Image::AUTO), + 100); + + // Instantiate default themes (site and admin) + foreach (array("wind", "admin_wind") as $theme_name) { + $theme_info = new ArrayObject(parse_ini_file(THEMEPATH . $theme_name . "/theme.info"), + ArrayObject::ARRAY_AS_PROPS); + $theme = ORM::factory("theme"); + $theme->name = $theme_name; + $theme->version = $theme_info->version; + $theme->save(); + } + + block_manager::add("dashboard_sidebar", "gallery", "block_adder"); + block_manager::add("dashboard_sidebar", "gallery", "stats"); + block_manager::add("dashboard_sidebar", "gallery", "platform_info"); + block_manager::add("dashboard_sidebar", "gallery", "project_news"); + block_manager::add("dashboard_center", "gallery", "welcome"); + block_manager::add("dashboard_center", "gallery", "upgrade_checker"); + block_manager::add("dashboard_center", "gallery", "photo_stream"); + block_manager::add("dashboard_center", "gallery", "log_entries"); + + module::set_var("gallery", "choose_default_tookit", 1); + module::set_var("gallery", "date_format", "Y-M-d"); + module::set_var("gallery", "date_time_format", "Y-M-d H:i:s"); + module::set_var("gallery", "time_format", "H:i:s"); + module::set_var("gallery", "show_credits", 1); + // Mark string for translation + $powered_by_string = t("Powered by <a href=\"%url\">%gallery_version</a>", + array("locale" => "root")); + module::set_var("gallery", "credits", (string) $powered_by_string); + module::set_var("gallery", "simultaneous_upload_limit", 5); + module::set_var("gallery", "admin_area_timeout", 90 * 60); + module::set_var("gallery", "maintenance_mode", 0); + module::set_var("gallery", "visible_title_length", 15); + module::set_var("gallery", "favicon_url", "lib/images/favicon.ico"); + module::set_var("gallery", "apple_touch_icon_url", "lib/images/apple-touch-icon.png"); + module::set_var("gallery", "email_from", ""); + module::set_var("gallery", "email_reply_to", ""); + module::set_var("gallery", "email_line_length", 70); + module::set_var("gallery", "email_header_separator", serialize("\n")); + module::set_var("gallery", "show_user_profiles_to", "registered_users"); + module::set_var("gallery", "extra_binary_paths", "/usr/local/bin:/opt/local/bin:/opt/bin"); + module::set_var("gallery", "timezone", null); + module::set_var("gallery", "lock_timeout", 1); + module::set_var("gallery", "movie_extract_frame_time", 3); + module::set_var("gallery", "movie_allow_uploads", "autodetect"); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + module::set_var("gallery", "date_format", "Y-M-d"); + module::set_var("gallery", "date_time_format", "Y-M-d H:i:s"); + module::set_var("gallery", "time_format", "H:i:s"); + module::set_version("gallery", $version = 2); + } + + if ($version == 2) { + module::set_var("gallery", "show_credits", 1); + module::set_version("gallery", $version = 3); + } + + if ($version == 3) { + $db->query("CREATE TABLE {caches} ( + `id` varchar(255) NOT NULL, + `tags` varchar(255), + `expiration` int(9) NOT NULL, + `cache` text, + PRIMARY KEY (`id`), + KEY (`tags`)) + DEFAULT CHARSET=utf8;"); + module::set_version("gallery", $version = 4); + } + + if ($version == 4) { + Cache::instance()->delete_all(); + $db->query("ALTER TABLE {caches} MODIFY COLUMN `cache` LONGBLOB"); + module::set_version("gallery", $version = 5); + } + + if ($version == 5) { + Cache::instance()->delete_all(); + $db->query("ALTER TABLE {caches} DROP COLUMN `id`"); + $db->query("ALTER TABLE {caches} ADD COLUMN `key` varchar(255) NOT NULL"); + $db->query("ALTER TABLE {caches} ADD COLUMN `id` int(9) NOT NULL auto_increment PRIMARY KEY"); + module::set_version("gallery", $version = 6); + } + + if ($version == 6) { + module::clear_var("gallery", "version"); + module::set_version("gallery", $version = 7); + } + + if ($version == 7) { + $groups = identity::groups(); + $permissions = ORM::factory("permission")->find_all(); + foreach($groups as $group) { + foreach($permissions as $permission) { + // Update access intents + $db->query("ALTER TABLE {access_intents} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) DEFAULT NULL"); + // Update access cache + if ($permission->name === "view") { + $db->query("ALTER TABLE {items} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) DEFAULT FALSE"); + } else { + $db->query("ALTER TABLE {access_caches} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) NOT NULL DEFAULT FALSE"); + } + } + } + module::set_version("gallery", $version = 8); + } + + if ($version == 8) { + $db->query("ALTER TABLE {items} CHANGE COLUMN `left` `left_ptr` INT(9) NOT NULL;"); + $db->query("ALTER TABLE {items} CHANGE COLUMN `right` `right_ptr` INT(9) NOT NULL;"); + module::set_version("gallery", $version = 9); + } + + if ($version == 9) { + $db->query("ALTER TABLE {items} ADD KEY `weight` (`weight` DESC);"); + + module::set_version("gallery", $version = 10); + } + + if ($version == 10) { + module::set_var("gallery", "image_sharpen", 15); + + module::set_version("gallery", $version = 11); + } + + if ($version == 11) { + $db->query("ALTER TABLE {items} ADD COLUMN `relative_url_cache` varchar(255) DEFAULT NULL"); + $db->query("ALTER TABLE {items} ADD COLUMN `slug` varchar(255) DEFAULT NULL"); + + // This is imperfect since some of the slugs may contain invalid characters, but it'll do + // for now because we don't want a lengthy operation here. + $db->query("UPDATE {items} SET `slug` = `name`"); + + // Flush all path caches because we're going to start urlencoding them. + $db->query("UPDATE {items} SET `relative_url_cache` = NULL, `relative_path_cache` = NULL"); + module::set_version("gallery", $version = 12); + } + + if ($version == 12) { + if (module::get_var("gallery", "active_site_theme") == "default") { + module::set_var("gallery", "active_site_theme", "wind"); + } + if (module::get_var("gallery", "active_admin_theme") == "admin_default") { + module::set_var("gallery", "active_admin_theme", "admin_wind"); + } + module::set_version("gallery", $version = 13); + } + + if ($version == 13) { + // Add rules for generating our thumbnails and resizes + Database::instance()->query( + "UPDATE {graphics_rules} SET `operation` = CONCAT('gallery_graphics::', `operation`);"); + module::set_version("gallery", $version = 14); + } + + if ($version == 14) { + $sidebar_blocks = block_manager::get_active("site_sidebar"); + if (empty($sidebar_blocks)) { + $available_blocks = block_manager::get_available_site_blocks(); + foreach (array_keys(block_manager::get_available_site_blocks()) as $id) { + $sidebar_blocks[] = explode(":", $id); + } + block_manager::set_active("site_sidebar", $sidebar_blocks); + } + module::set_version("gallery", $version = 15); + } + + if ($version == 15) { + module::set_var("gallery", "identity_provider", "user"); + module::set_version("gallery", $version = 16); + } + + // Convert block keys to an md5 hash of the module and block name + if ($version == 16) { + foreach (array("dashboard_sidebar", "dashboard_center", "site_sidebar") as $location) { + $blocks = block_manager::get_active($location); + $new_blocks = array(); + foreach ($blocks as $block) { + $new_blocks[md5("{$block[0]}:{$block[1]}")] = $block; + } + block_manager::set_active($location, $new_blocks); + } + module::set_version("gallery", $version = 17); + } + + // We didn't like md5 hashes so convert block keys back to random keys to allow duplicates. + if ($version == 17) { + foreach (array("dashboard_sidebar", "dashboard_center", "site_sidebar") as $location) { + $blocks = block_manager::get_active($location); + $new_blocks = array(); + foreach ($blocks as $block) { + $new_blocks[random::int()] = $block; + } + block_manager::set_active($location, $new_blocks); + } + module::set_version("gallery", $version = 18); + } + + // Rename blocks_site.sidebar to blocks_site_sidebar + if ($version == 18) { + $blocks = block_manager::get_active("site.sidebar"); + block_manager::set_active("site_sidebar", $blocks); + module::clear_var("gallery", "blocks_site.sidebar"); + module::set_version("gallery", $version = 19); + } + + // Set a default for the number of simultaneous uploads + // Version 20 was reverted in 57adefc5baa7a2b0dfcd3e736e80c2fa86d3bfa2, so skip it. + if ($version == 19 || $version == 20) { + module::set_var("gallery", "simultaneous_upload_limit", 5); + module::set_version("gallery", $version = 21); + } + + // Update the graphics rules table so that the maximum height for resizes is 640 not 480. + // Fixes ticket #671 + if ($version == 21) { + $resize_rule = ORM::factory("graphics_rule") + ->where("id", "=", "2") + ->find(); + // make sure it hasn't been changed already + $args = unserialize($resize_rule->args); + if ($args["height"] == 480 && $args["width"] == 640) { + $args["height"] = 640; + $resize_rule->args = serialize($args); + $resize_rule->save(); + } + module::set_version("gallery", $version = 22); + } + + // Update slug values to be legal. We should have done this in the 11->12 upgrader, but I was + // lazy. Mea culpa! + if ($version == 22) { + foreach (db::build() + ->from("items") + ->select("id", "slug") + ->where(db::expr("`slug` REGEXP '[^_A-Za-z0-9-]'"), "=", 1) + ->execute() as $row) { + $new_slug = item::convert_filename_to_slug($row->slug); + if (empty($new_slug)) { + $new_slug = random::int(); + } + db::build() + ->update("items") + ->set("slug", $new_slug) + ->set("relative_url_cache", null) + ->where("id", "=", $row->id) + ->execute(); + } + module::set_version("gallery", $version = 23); + } + + if ($version == 23) { + $db->query("CREATE TABLE {failed_logins} ( + `id` int(9) NOT NULL auto_increment, + `count` int(9) NOT NULL, + `name` varchar(255) NOT NULL, + `time` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("gallery", $version = 24); + } + + if ($version == 24) { + foreach (array("logs", "tmp", "uploads") as $dir) { + self::_protect_directory(VARPATH . $dir); + } + module::set_version("gallery", $version = 25); + } + + if ($version == 25) { + db::build() + ->update("items") + ->set("title", db::expr("`name`")) + ->and_open() + ->where("title", "IS", null) + ->or_where("title", "=", "") + ->close() + ->execute(); + module::set_version("gallery", $version = 26); + } + + if ($version == 26) { + if (in_array("failed_logins", Database::instance()->list_tables())) { + $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); + } + + if ($version == 28) { + module::set_var("gallery", "credits", "Powered by <a href=\"%url\">%gallery_version</a>"); + module::set_version("gallery", $version = 29); + } + + if ($version == 29) { + $db->query("ALTER TABLE {caches} ADD KEY (`key`);"); + module::set_version("gallery", $version = 30); + } + + if ($version == 30) { + module::set_var("gallery", "maintenance_mode", 0); + module::set_version("gallery", $version = 31); + } + + if ($version == 31) { + $db->query("ALTER TABLE {modules} ADD COLUMN `weight` int(9) DEFAULT NULL"); + $db->query("ALTER TABLE {modules} ADD KEY (`weight`)"); + db::update("modules") + ->set("weight", db::expr("`id`")) + ->execute(); + module::set_version("gallery", $version = 32); + } + + if ($version == 32) { + $db->query("ALTER TABLE {items} ADD KEY (`left_ptr`)"); + module::set_version("gallery", $version = 33); + } + + if ($version == 33) { + $db->query("ALTER TABLE {access_caches} ADD KEY (`item_id`)"); + module::set_version("gallery", $version = 34); + } + + if ($version == 34) { + module::set_var("gallery", "visible_title_length", 15); + module::set_version("gallery", $version = 35); + } + + if ($version == 35) { + module::set_var("gallery", "favicon_url", "lib/images/favicon.ico"); + module::set_version("gallery", $version = 36); + } + + if ($version == 36) { + module::set_var("gallery", "email_from", "admin@example.com"); + module::set_var("gallery", "email_reply_to", "public@example.com"); + module::set_var("gallery", "email_line_length", 70); + module::set_var("gallery", "email_header_separator", serialize("\n")); + module::set_version("gallery", $version = 37); + } + + // Changed our minds and decided that the initial value should be empty + // But don't just reset it blindly, only do it if the value is version 37 default + if ($version == 37) { + $email = module::get_var("gallery", "email_from", ""); + if ($email == "admin@example.com") { + module::set_var("gallery", "email_from", ""); + } + $email = module::get_var("gallery", "email_reply_to", ""); + if ($email == "admin@example.com") { + module::set_var("gallery", "email_reply_to", ""); + } + module::set_version("gallery", $version = 38); + } + + if ($version == 38) { + module::set_var("gallery", "show_user_profiles_to", "registered_users"); + module::set_version("gallery", $version = 39); + } + + if ($version == 39) { + module::set_var("gallery", "extra_binary_paths", "/usr/local/bin:/opt/local/bin:/opt/bin"); + module::set_version("gallery", $version = 40); + } + + if ($version == 40) { + module::clear_var("gallery", "_cache"); + module::set_version("gallery", $version = 41); + } + + if ($version == 41) { + $db->query("TRUNCATE TABLE {caches}"); + $db->query("ALTER TABLE {caches} DROP INDEX `key`, ADD UNIQUE `key` (`key`)"); + module::set_version("gallery", $version = 42); + } + + if ($version == 42) { + $db->query("ALTER TABLE {items} CHANGE `description` `description` text DEFAULT NULL"); + module::set_version("gallery", $version = 43); + } + + if ($version == 43) { + $db->query("ALTER TABLE {items} CHANGE `rand_key` `rand_key` DECIMAL(11, 10)"); + module::set_version("gallery", $version = 44); + } + + if ($version == 44) { + $db->query("ALTER TABLE {messages} CHANGE `value` `value` text default NULL"); + module::set_version("gallery", $version = 45); + } + + if ($version == 45) { + // Splice the upgrade_checker block into the admin dashboard at the top + // of the page, but under the welcome block if it's in the first position. + $blocks = block_manager::get_active("dashboard_center"); + $index = count($blocks) && current($blocks) == array("gallery", "welcome") ? 1 : 0; + array_splice($blocks, $index, 0, array(random::int() => array("gallery", "upgrade_checker"))); + block_manager::set_active("dashboard_center", $blocks); + + module::set_var("gallery", "upgrade_checker_auto_enabled", true); + module::set_version("gallery", $version = 46); + } + + if ($version == 46) { + module::set_var("gallery", "apple_touch_icon_url", "lib/images/apple-touch-icon.png"); + module::set_version("gallery", $version = 47); + } + + if ($version == 47 || $version == 48) { + // Add configuration variable to set timezone. Defaults to the currently + // used timezone (from PHP configuration). Note that in v48 we were + // setting this value incorrectly, so we're going to stomp this value for v49. + module::set_var("gallery", "timezone", null); + module::set_version("gallery", $version = 49); + } + + if ($version == 49) { + // In v49 we changed the Item_Model validation code to disallow files with two dots in them, + // but we didn't rename any files which fail to validate, so as soon as you do anything to + // change those files (eg. as a side effect of getting the url or file path) it fails to + // validate. Fix those here. This might be slow, but if it times out it can just pick up + // where it left off. + foreach (db::build() + ->from("items") + ->select("id") + ->where("type", "<>", "album") + ->where(db::expr("`name` REGEXP '\\\\..*\\\\.'"), "=", 1) + ->order_by("id", "asc") + ->execute() as $row) { + set_time_limit(30); + $item = ORM::factory("item", $row->id); + $item->name = legal_file::smash_extensions($item->name); + $item->save(); + } + module::set_version("gallery", $version = 50); + } + + if ($version == 50) { + // In v51, we added a lock_timeout variable so that administrators could edit the time out + // from 1 second to a higher variable if their system runs concurrent parallel uploads for + // instance. + module::set_var("gallery", "lock_timeout", 1); + module::set_version("gallery", $version = 51); + } + + if ($version == 51) { + // In v52, we added functions to the legal_file helper that map photo and movie file + // extensions to their mime types (and allow extension of the list by other modules). During + // this process, we correctly mapped m4v files to video/x-m4v, correcting a previous error + // where they were mapped to video/mp4. This corrects the existing items. + db::build() + ->update("items") + ->set("mime_type", "video/x-m4v") + ->where("name", "REGEXP", "\.m4v$") // case insensitive since name column is utf8_general_ci + ->execute(); + module::set_version("gallery", $version = 52); + } + + if ($version == 52) { + // In v53, we added the ability to change the default time used when extracting frames from + // movies. Previously we hard-coded this at 3 seconds, so we use that as the default. + module::set_var("gallery", "movie_extract_frame_time", 3); + module::set_version("gallery", $version = 53); + } + + if ($version == 53) { + // In v54, we changed how we check for name and slug conflicts in Item_Model. Previously, + // we checked the whole filename. As a result, "foo.jpg" and "foo.png" were not considered + // conflicting if their slugs were different (a rare case in practice since server_add and + // uploader would give them both the same slug "foo"). Now, we check the filename without its + // extension. This upgrade stanza fixes any conflicts where they were previously allowed. + + // This might be slow, but if it times out it can just pick up where it left off. + + // Find and loop through each conflict (e.g. "foo.jpg", "foo.png", and "foo.flv" are one + // conflict; "bar.jpg", "bar.png", and "bar.flv" are another) + foreach (db::build() + ->select_distinct(array("parent_base_name" => + db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))"))) + ->select(array("C" => "COUNT(\"*\")")) + ->from("items") + ->where("type", "<>", "album") + ->having("C", ">", 1) + ->group_by("parent_base_name") + ->execute() as $conflict) { + list ($parent_id, $base_name) = explode(":", $conflict->parent_base_name, 2); + $base_name_escaped = Database::escape_for_like($base_name); + // Loop through the items for each conflict + foreach (db::build() + ->from("items") + ->select("id") + ->where("type", "<>", "album") + ->where("parent_id", "=", $parent_id) + ->where("name", "LIKE", "{$base_name_escaped}.%") + ->limit(1000000) // required to satisfy SQL syntax (no offset without limit) + ->offset(1) // skips the 0th item + ->execute() as $row) { + set_time_limit(30); + $item = ORM::factory("item", $row->id); + $item->name = $item->name; // this will force Item_Model to check for conflicts on save + $item->save(); + } + } + module::set_version("gallery", $version = 54); + } + + if ($version == 54) { + $db->query("ALTER TABLE {items} ADD KEY `relative_path_cache` (`relative_path_cache`)"); + module::set_version("gallery", $version = 55); + } + + if ($version == 55) { + // In v56, we added the ability to change the default behavior regarding movie uploads. It + // can be set to "always", "never", or "autodetect" to match the previous behavior where they + // are allowed only if FFmpeg is found. + module::set_var("gallery", "movie_allow_uploads", "autodetect"); + module::set_version("gallery", $version = 56); + } + + if ($version == 56) { + // Cleanup possible instances where resize_dirty of albums or movies was set to 0. This is + // unlikely to have occurred, and doesn't currently matter much since albums and movies don't + // have resize images anyway. However, it may be useful to be consistent here going forward. + db::build() + ->update("items") + ->set("resize_dirty", 1) + ->where("type", "<>", "photo") + ->execute(); + module::set_version("gallery", $version = 57); + } + } + + static function uninstall() { + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {access_caches}"); + $db->query("DROP TABLE IF EXISTS {access_intents}"); + $db->query("DROP TABLE IF EXISTS {graphics_rules}"); + $db->query("DROP TABLE IF EXISTS {incoming_translations}"); + $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}"); + $db->query("DROP TABLE IF EXISTS {outgoing_translations}"); + $db->query("DROP TABLE IF EXISTS {permissions}"); + $db->query("DROP TABLE IF EXISTS {sessions}"); + $db->query("DROP TABLE IF EXISTS {tasks}"); + $db->query("DROP TABLE IF EXISTS {themes}"); + $db->query("DROP TABLE IF EXISTS {vars}"); + $db->query("DROP TABLE IF EXISTS {caches}"); + foreach (array("albums", "resizes", "thumbs", "uploads", + "modules", "logs", "database.php") as $entry) { + system("/bin/rm -rf " . VARPATH . $entry); + } + } + + static function _protect_directory($dir) { + $fp = @fopen("$dir/.htaccess", "w+"); + fwrite($fp, "DirectoryIndex .htaccess\nSetHandler Gallery_Security_Do_Not_Remove\n" . + "Options None\n<IfModule mod_rewrite.c>\nRewriteEngine off\n</IfModule>\n" . + "Order allow,deny\nDeny from all\n"); + fclose($fp); + } +} diff --git a/modules/gallery/helpers/gallery_rss.php b/modules/gallery/helpers/gallery_rss.php new file mode 100644 index 0000000..d6b3302 --- /dev/null +++ b/modules/gallery/helpers/gallery_rss.php @@ -0,0 +1,76 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class gallery_rss_Core { + static function available_feeds($item, $tag) { + $feeds["gallery/latest"] = t("Latest photos and movies"); + + if ($item) { + $feed_item = $item -> is_album() ? $item : $item->parent(); + + $feeds["gallery/album/{$feed_item->id}"] = + t("%title photos and movies", array("title" => $feed_item->title)); + } + + return $feeds; + } + + static function feed($feed_id, $offset, $limit, $id) { + $feed = new stdClass(); + switch ($feed_id) { + case "latest": + $feed->items = ORM::factory("item") + ->viewable() + ->where("type", "<>", "album") + ->order_by("created", "DESC") + ->find_all($limit, $offset); + + $all_items = ORM::factory("item") + ->viewable() + ->where("type", "<>", "album") + ->order_by("created", "DESC"); + + $feed->max_pages = ceil($all_items->find_all()->count() / $limit); + $feed->title = t("%site_title - Recent updates", array("site_title" => item::root()->title)); + $feed->description = t("Recent updates"); + return $feed; + + case "album": + $item = ORM::factory("item", $id); + access::required("view", $item); + + $feed->items = $item + ->viewable() + ->descendants($limit, $offset, array(array("type", "=", "photo"))); + $feed->max_pages = ceil( + $item->viewable()->descendants_count(array(array("type", "=", "photo"))) / $limit); + if ($item->id == item::root()->id) { + $feed->title = html::purify($item->title); + } else { + $feed->title = t("%site_title - %item_title", + array("site_title" => item::root()->title, + "item_title" => $item->title)); + } + $feed->description = nl2br(html::purify($item->description)); + + return $feed; + } + } +} diff --git a/modules/gallery/helpers/gallery_task.php b/modules/gallery/helpers/gallery_task.php new file mode 100644 index 0000000..618cf8f --- /dev/null +++ b/modules/gallery/helpers/gallery_task.php @@ -0,0 +1,826 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_task_Core { + const FIX_STATE_START_MPTT = 0; + const FIX_STATE_RUN_MPTT = 1; + const FIX_STATE_START_ALBUMS = 2; + const FIX_STATE_RUN_ALBUMS = 3; + const FIX_STATE_START_DUPE_SLUGS = 4; + const FIX_STATE_RUN_DUPE_SLUGS = 5; + const FIX_STATE_START_DUPE_NAMES = 6; + const FIX_STATE_RUN_DUPE_NAMES = 7; + const FIX_STATE_START_DUPE_BASE_NAMES = 8; + const FIX_STATE_RUN_DUPE_BASE_NAMES = 9; + const FIX_STATE_START_REBUILD_ITEM_CACHES = 10; + const FIX_STATE_RUN_REBUILD_ITEM_CACHES = 11; + const FIX_STATE_START_MISSING_ACCESS_CACHES = 12; + const FIX_STATE_RUN_MISSING_ACCESS_CACHES = 13; + const FIX_STATE_DONE = 14; + + static function available_tasks() { + $dirty_count = graphics::find_dirty_images_query()->count_records(); + $tasks = array(); + $tasks[] = Task_Definition::factory() + ->callback("gallery_task::rebuild_dirty_images") + ->name(t("Rebuild Images")) + ->description($dirty_count ? + t2("You have one out of date photo", + "You have %count out of date photos", + $dirty_count) + : t("All your photos are up to date")) + ->severity($dirty_count ? log::WARNING : log::SUCCESS); + + $tasks[] = Task_Definition::factory() + ->callback("gallery_task::update_l10n") + ->name(t("Update translations")) + ->description(t("Download new and updated translated strings")) + ->severity(log::SUCCESS); + + $tasks[] = Task_Definition::factory() + ->callback("gallery_task::file_cleanup") + ->name(t("Remove old files")) + ->description(t("Remove expired files from the logs and tmp directory")) + ->severity(log::SUCCESS); + + $tasks[] = Task_Definition::factory() + ->callback("gallery_task::fix") + ->name(t("Fix your Gallery")) + ->description(t("Fix a variety of problems that might cause your Gallery to act strangely. Requires maintenance mode.")) + ->severity(log::SUCCESS); + + return $tasks; + } + + /** + * Task that rebuilds all dirty images. + * @param Task_Model the task + */ + static function rebuild_dirty_images($task) { + $errors = array(); + try { + // Choose the dirty images in a random order so that if we run this task multiple times + // concurrently each task is rebuilding different images simultaneously. + $result = graphics::find_dirty_images_query()->select("id") + ->select(db::expr("RAND() as r")) + ->order_by("r", "ASC") + ->execute(); + $total_count = $task->get("total_count", $result->count()); + $mode = $task->get("mode", "init"); + if ($mode == "init") { + $task->set("total_count", $total_count); + $task->set("mode", "process"); + batch::start(); + } + + $completed = $task->get("completed", 0); + $ignored = $task->get("ignored", array()); + + $i = 0; + + // If there's no work left to do, skip to the end. This can happen if we resume a task long + // after the work got done in some other task. + if (!$result->count()) { + $completed = $total_count; + } + + foreach ($result as $row) { + if (array_key_exists($row->id, $ignored)) { + continue; + } + + $item = ORM::factory("item", $row->id); + if ($item->loaded()) { + try { + graphics::generate($item); + $completed++; + + $errors[] = t("Successfully rebuilt images for '%title'", + array("title" => html::purify($item->title))); + } catch (Exception $e) { + $errors[] = t("Unable to rebuild images for '%title'", + array("title" => html::purify($item->title))); + $errors[] = (string)$e; + $ignored[$item->id] = 1; + } + } + + if (++$i == 2) { + break; + } + } + + $task->status = t2("Updated: 1 image. Total: %total_count.", + "Updated: %count images. Total: %total_count.", + $completed, + array("total_count" => $total_count)); + + if ($completed < $total_count) { + $task->percent_complete = (int)(100 * ($completed + count($ignored)) / $total_count); + } else { + $task->percent_complete = 100; + } + + $task->set("completed", $completed); + $task->set("ignored", $ignored); + if ($task->percent_complete == 100) { + $task->done = true; + $task->state = "success"; + batch::stop(); + site_status::clear("graphics_dirty"); + } + } catch (Exception $e) { + Kohana_Log::add("error",(string)$e); + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $errors[] = (string)$e; + } + if ($errors) { + $task->log($errors); + } + } + + static function update_l10n($task) { + $errors = array(); + try { + $start = microtime(true); + $data = Cache::instance()->get("update_l10n_cache:{$task->id}"); + if ($data) { + list($dirs, $files, $cache, $num_fetched) = unserialize($data); + } + $i = 0; + + switch ($task->get("mode", "init")) { + case "init": // 0% + $dirs = array("gallery", "modules", "themes", "installer"); + $files = $cache = array(); + $num_fetched = 0; + $task->set("mode", "find_files"); + $task->status = t("Finding files"); + break; + + case "find_files": // 0% - 10% + while (($dir = array_pop($dirs)) && microtime(true) - $start < 0.5) { + if (in_array(basename($dir), array("tests", "lib"))) { + continue; + } + + foreach (glob(DOCROOT . "$dir/*") as $path) { + $relative_path = str_replace(DOCROOT, "", $path); + if (is_dir($path)) { + $dirs[] = $relative_path; + } else { + $files[] = $relative_path; + } + } + } + + $task->status = t2("Finding files: found 1 file", + "Finding files: found %count files", count($files)); + + if (!$dirs) { + $task->set("mode", "scan_files"); + $task->set("total_files", count($files)); + $task->status = t("Scanning files"); + $task->percent_complete = 10; + } + break; + + case "scan_files": // 10% - 70% + while (($file = array_pop($files)) && microtime(true) - $start < 0.5) { + $file = DOCROOT . $file; + switch (pathinfo($file, PATHINFO_EXTENSION)) { + case "php": + l10n_scanner::scan_php_file($file, $cache); + break; + + case "info": + l10n_scanner::scan_info_file($file, $cache); + break; + } + } + + $total_files = $task->get("total_files"); + $task->status = t2("Scanning files: scanned 1 file", + "Scanning files: scanned %count files", $total_files - count($files)); + + $task->percent_complete = 10 + 60 * ($total_files - count($files)) / $total_files; + if (empty($files)) { + $task->set("mode", "fetch_updates"); + $task->status = t("Fetching updates"); + $task->percent_complete = 70; + } + break; + + case "fetch_updates": // 70% - 100% + // Send fetch requests in batches until we're done + $num_remaining = l10n_client::fetch_updates($num_fetched); + if ($num_remaining) { + $total = $num_fetched + $num_remaining; + $task->percent_complete = 70 + 30 * ((float) $num_fetched / $total); + } else { + Gallery_I18n::clear_cache(); + + $task->done = true; + $task->state = "success"; + $task->status = t("Translations installed/updated"); + $task->percent_complete = 100; + } + } + + if (!$task->done) { + Cache::instance()->set("update_l10n_cache:{$task->id}", + serialize(array($dirs, $files, $cache, $num_fetched)), + array("l10n")); + } else { + Cache::instance()->delete("update_l10n_cache:{$task->id}"); + } + } catch (Exception $e) { + Kohana_Log::add("error",(string)$e); + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $errors[] = (string)$e; + } + if ($errors) { + $task->log($errors); + } + } + + /** + * Task that removes old files from var/logs and var/tmp. + * @param Task_Model the task + */ + static function file_cleanup($task) { + $errors = array(); + try { + $start = microtime(true); + $data = Cache::instance()->get("file_cleanup_cache:{$task->id}"); + $files = $data ? unserialize($data) : array(); + $i = 0; + $current = 0; + $total = 0; + + switch ($task->get("mode", "init")) { + case "init": + $threshold = time() - 1209600; // older than 2 weeks + // Note that this code is roughly duplicated in gallery_event::gallery_shutdown + foreach(array("logs", "tmp") as $dir) { + $dir = VARPATH . $dir; + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false) { + if ($file[0] == ".") { + continue; + } + + // Ignore directories for now, but we should really address them in the long term. + if (is_dir("$dir/$file")) { + continue; + } + + if (filemtime("$dir/$file") <= $threshold) { + $files[] = "$dir/$file"; + } + } + } + } + $task->set("mode", "delete_files"); + $task->set("current", 0); + $task->set("total", count($files)); + Cache::instance()->set("file_cleanup_cache:{$task->id}", serialize($files), + array("file_cleanup")); + if (count($files) == 0) { + break; + } + + case "delete_files": + $current = $task->get("current"); + $total = $task->get("total"); + while ($current < $total && microtime(true) - $start < 1) { + @unlink($files[$current]); + $task->log(t("%file removed", array("file" => $files[$current++]))); + } + $task->percent_complete = $current / $total * 100; + $task->set("current", $current); + } + + $task->status = t2("Removed: 1 file. Total: %total_count.", + "Removed: %count files. Total: %total_count.", + $current, array("total_count" => $total)); + + if ($total == $current) { + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + } + } catch (Exception $e) { + Kohana_Log::add("error",(string)$e); + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $errors[] = (string)$e; + } + if ($errors) { + $task->log($errors); + } + } + + static function fix($task) { + $start = microtime(true); + + $total = $task->get("total"); + if (empty($total)) { + $item_count = db::build()->count_records("items"); + $total = 0; + + // mptt: 2 operations for every item + $total += 2 * $item_count; + + // album audit (permissions and bogus album covers): 1 operation for every album + $total += db::build()->where("type", "=", "album")->count_records("items"); + + // one operation for each dupe slug, dupe name, dupe base name, and missing access cache + foreach (array("find_dupe_slugs", "find_dupe_names", "find_dupe_base_names", + "find_missing_access_caches") as $func) { + foreach (self::$func() as $row) { + $total++; + } + } + + // one operation to rebuild path and url caches; + $total += 1 * $item_count; + + $task->set("total", $total); + $task->set("state", $state = self::FIX_STATE_START_MPTT); + $task->set("ptr", 1); + $task->set("completed", 0); + } + + $completed = $task->get("completed"); + $state = $task->get("state"); + + if (!module::get_var("gallery", "maintenance_mode")) { + module::set_var("gallery", "maintenance_mode", 1); + } + + // This is a state machine that checks each item in the database. It verifies the following + // attributes for an item. + // 1. Left and right MPTT pointers are correct + // 2. The .htaccess permission files for restricted items exist and are well formed. + // 3. The relative_path_cache and relative_url_cache values are set to null. + // 4. there are no album_cover_item_ids pointing to missing items + // + // We'll do a depth-first tree walk over our hierarchy using only the adjacency data because + // we don't trust MPTT here (that might be what we're here to fix!). Avoid avoid using ORM + // calls as much as possible since they're expensive. + // + // NOTE: the MPTT check will only traverse items that have valid parents. It's possible that + // we have some tree corruption where there are items with parent ids to non-existent items. + // We should probably do something about that. + while ($state != self::FIX_STATE_DONE && microtime(true) - $start < 1.5) { + switch ($state) { + case self::FIX_STATE_START_MPTT: + $task->set("ptr", $ptr = 1); + $task->set("stack", item::root()->id . "L1"); + $state = self::FIX_STATE_RUN_MPTT; + break; + + case self::FIX_STATE_RUN_MPTT: + $ptr = $task->get("ptr"); + $stack = explode(" ", $task->get("stack")); + preg_match("/([0-9]+)([A-Z])([0-9]+)/", array_pop($stack), $matches); // e.g. "12345L10" + list ( , $id, $ptr_mode, $level) = $matches; // Skip the 0th entry of matches. + switch ($ptr_mode) { + case "L": + // Albums could be parent nodes. + $stack[] = "{$id}R{$level}"; + db::build() + ->update("items") + ->set("left_ptr", $ptr++) + ->where("id", "=", $id) + ->execute(); + + $level++; + foreach (db::build() + ->select(array("id", "type")) + ->from("items") + ->where("parent_id", "=", $id) + ->order_by("left_ptr", "DESC") // DESC since array_pop effectively reverses them + ->execute() as $child) { + $stack[] = ($child->type == "album") ? "{$child->id}L{$level}" : "{$child->id}B{$level}"; + } + $completed++; + break; + case "B": + // Non-albums must be leaf nodes. + db::build() + ->update("items") + ->set("left_ptr", $ptr++) + ->set("right_ptr", $ptr++) + ->set("level", $level) + ->set("relative_path_cache", null) + ->set("relative_url_cache", null) + ->where("id", "=", $id) + ->execute(); + $completed += 2; // we updated two pointers + break; + case "R": + db::build() + ->update("items") + ->set("right_ptr", $ptr++) + ->set("level", $level) + ->set("relative_path_cache", null) + ->set("relative_url_cache", null) + ->where("id", "=", $id) + ->execute(); + $completed++; + } + $task->set("ptr", $ptr); + $task->set("stack", implode(" ", $stack)); + + if (empty($stack)) { + $state = self::FIX_STATE_START_DUPE_SLUGS; + } + break; + + + case self::FIX_STATE_START_DUPE_SLUGS: + $stack = array(); + foreach (self::find_dupe_slugs() as $row) { + list ($parent_id, $slug) = explode(":", $row->parent_slug, 2); + $stack[] = join(":", array($parent_id, $slug)); + } + if ($stack) { + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_DUPE_SLUGS; + } else { + $state = self::FIX_STATE_START_DUPE_NAMES; + } + break; + + case self::FIX_STATE_RUN_DUPE_SLUGS: + $stack = explode(" ", $task->get("stack")); + list ($parent_id, $slug) = explode(":", array_pop($stack)); + + // We want to leave the first one alone and update all conflicts to be random values. + $fixed = 0; + $conflicts = ORM::factory("item") + ->where("parent_id", "=", $parent_id) + ->where("slug", "=", $slug) + ->find_all(1, 1); + if ($conflicts->count() && $conflict = $conflicts->current()) { + $task->log("Fixing conflicting slug for item id {$conflict->id}"); + db::build() + ->update("items") + ->set("slug", $slug . "-" . (string)rand(1000, 9999)) + ->where("id", "=", $conflict->id) + ->execute(); + + // We fixed one conflict, but there might be more so put this parent back on the stack + // and try again. We won't consider it completed when we don't fix a conflict. This + // guarantees that we won't spend too long fixing one set of conflicts, and that we + // won't stop before all are fixed. + $stack[] = "$parent_id:$slug"; + break; + } + $task->set("stack", implode(" ", $stack)); + $completed++; + + if (empty($stack)) { + $state = self::FIX_STATE_START_DUPE_NAMES; + } + break; + + case self::FIX_STATE_START_DUPE_NAMES: + $stack = array(); + foreach (self::find_dupe_names() as $row) { + list ($parent_id, $name) = explode(":", $row->parent_name, 2); + $stack[] = join(":", array($parent_id, $name)); + } + if ($stack) { + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_DUPE_NAMES; + } else { + $state = self::FIX_STATE_START_DUPE_BASE_NAMES; + } + break; + + case self::FIX_STATE_RUN_DUPE_NAMES: + // NOTE: This does *not* attempt to fix the file system! + $stack = explode(" ", $task->get("stack")); + list ($parent_id, $name) = explode(":", array_pop($stack)); + + $fixed = 0; + // We want to leave the first one alone and update all conflicts to be random values. + $conflicts = ORM::factory("item") + ->where("parent_id", "=", $parent_id) + ->where("name", "=", $name) + ->find_all(1, 1); + if ($conflicts->count() && $conflict = $conflicts->current()) { + $task->log("Fixing conflicting name for item id {$conflict->id}"); + if (!$conflict->is_album() && preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) { + $item_base_name = $matches[1]; + $item_extension = $matches[2]; // includes a leading dot + } else { + $item_base_name = $conflict->name; + $item_extension = ""; + } + db::build() + ->update("items") + ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension) + ->where("id", "=", $conflict->id) + ->execute(); + + // We fixed one conflict, but there might be more so put this parent back on the stack + // and try again. We won't consider it completed when we don't fix a conflict. This + // guarantees that we won't spend too long fixing one set of conflicts, and that we + // won't stop before all are fixed. + $stack[] = "$parent_id:$name"; + break; + } + $task->set("stack", implode(" ", $stack)); + $completed++; + + if (empty($stack)) { + $state = self::FIX_STATE_START_DUPE_BASE_NAMES; + } + break; + + case self::FIX_STATE_START_DUPE_BASE_NAMES: + $stack = array(); + foreach (self::find_dupe_base_names() as $row) { + list ($parent_id, $base_name) = explode(":", $row->parent_base_name, 2); + $stack[] = join(":", array($parent_id, $base_name)); + } + if ($stack) { + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_DUPE_BASE_NAMES; + } else { + $state = self::FIX_STATE_START_ALBUMS; + } + break; + + case self::FIX_STATE_RUN_DUPE_BASE_NAMES: + // NOTE: This *does* attempt to fix the file system! So, it must go *after* run_dupe_names. + $stack = explode(" ", $task->get("stack")); + list ($parent_id, $base_name) = explode(":", array_pop($stack)); + $base_name_escaped = Database::escape_for_like($base_name); + + $fixed = 0; + // We want to leave the first one alone and update all conflicts to be random values. + $conflicts = ORM::factory("item") + ->where("parent_id", "=", $parent_id) + ->where("name", "LIKE", "{$base_name_escaped}.%") + ->where("type", "<>", "album") + ->find_all(1, 1); + if ($conflicts->count() && $conflict = $conflicts->current()) { + $task->log("Fixing conflicting name for item id {$conflict->id}"); + if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) { + $item_base_name = $matches[1]; // unlike $base_name, this always maintains capitalization + $item_extension = $matches[2]; // includes a leading dot + } else { + $item_base_name = $conflict->name; + $item_extension = ""; + } + // Unlike conflicts found in run_dupe_names, these items are likely to have an intact + // file system. Let's use the item save logic to rebuild the paths and rename the files + // if possible. + try { + $conflict->name = $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension; + $conflict->validate(); + // If we get here, we're safe to proceed with save + $conflict->save(); + } catch (Exception $e) { + // Didn't work. Edit database directly without fixing file system. + db::build() + ->update("items") + ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension) + ->where("id", "=", $conflict->id) + ->execute(); + } + + // We fixed one conflict, but there might be more so put this parent back on the stack + // and try again. We won't consider it completed when we don't fix a conflict. This + // guarantees that we won't spend too long fixing one set of conflicts, and that we + // won't stop before all are fixed. + $stack[] = "$parent_id:$base_name"; + break; + } + $task->set("stack", implode(" ", $stack)); + $completed++; + + if (empty($stack)) { + $state = self::FIX_STATE_START_ALBUMS; + } + break; + + case self::FIX_STATE_START_ALBUMS: + $stack = array(); + foreach (db::build() + ->select("id") + ->from("items") + ->where("type", "=", "album") + ->execute() as $row) { + $stack[] = $row->id; + } + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_ALBUMS; + break; + + case self::FIX_STATE_RUN_ALBUMS: + $stack = explode(" ", $task->get("stack")); + $id = array_pop($stack); + + $item = ORM::factory("item", $id); + if ($item->album_cover_item_id) { + $album_cover_item = ORM::factory("item", $item->album_cover_item_id); + if (!$album_cover_item->loaded()) { + $item->album_cover_item_id = null; + $item->save(); + } + } + + $everybody = identity::everybody(); + $view_col = "view_{$everybody->id}"; + $view_full_col = "view_full_{$everybody->id}"; + $intent = ORM::factory("access_intent")->where("item_id", "=", $id)->find(); + if ($intent->$view_col === access::DENY) { + access::update_htaccess_files($item, $everybody, "view", access::DENY); + } + if ($intent->$view_full_col === access::DENY) { + access::update_htaccess_files($item, $everybody, "view_full", access::DENY); + } + $task->set("stack", implode(" ", $stack)); + $completed++; + + if (empty($stack)) { + $state = self::FIX_STATE_START_REBUILD_ITEM_CACHES; + } + break; + + case self::FIX_STATE_START_REBUILD_ITEM_CACHES: + $stack = array(); + foreach (self::find_empty_item_caches(500) as $row) { + $stack[] = $row->id; + } + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_REBUILD_ITEM_CACHES; + break; + + case self::FIX_STATE_RUN_REBUILD_ITEM_CACHES: + $stack = explode(" ", $task->get("stack")); + if (!empty($stack)) { + $id = array_pop($stack); + $item = ORM::factory("item", $id); + $item->relative_path(); // this rebuilds the cache and saves the item as a side-effect + $task->set("stack", implode(" ", $stack)); + $completed++; + } + + if (empty($stack)) { + // Try refilling the stack + foreach (self::find_empty_item_caches(500) as $row) { + $stack[] = $row->id; + } + $task->set("stack", implode(" ", $stack)); + + if (empty($stack)) { + $state = self::FIX_STATE_START_MISSING_ACCESS_CACHES; + } + } + break; + + case self::FIX_STATE_START_MISSING_ACCESS_CACHES: + $stack = array(); + foreach (self::find_missing_access_caches_limited(500) as $row) { + $stack[] = $row->id; + } + $task->set("stack", implode(" ", $stack)); + $state = self::FIX_STATE_RUN_MISSING_ACCESS_CACHES; + break; + + case self::FIX_STATE_RUN_MISSING_ACCESS_CACHES: + $stack = array_filter(explode(" ", $task->get("stack"))); // filter removes empty/zero ids + if (!empty($stack)) { + $id = array_pop($stack); + $access_cache = ORM::factory("access_cache"); + $access_cache->item_id = $id; + $access_cache->save(); + $task->set("stack", implode(" ", $stack)); + $completed++; + } + + if (empty($stack)) { + // Try refilling the stack + foreach (self::find_missing_access_caches_limited(500) as $row) { + $stack[] = $row->id; + } + $task->set("stack", implode(" ", $stack)); + + if (empty($stack)) { + // The new cache rows are there, but they're incorrectly populated so we have to fix + // them. If this turns out to be too slow, we'll have to refactor + // access::recalculate_permissions to allow us to do it in slices. + access::recalculate_album_permissions(item::root()); + $state = self::FIX_STATE_DONE; + } + } + break; + } + } + + $task->set("state", $state); + $task->set("completed", $completed); + + if ($state == self::FIX_STATE_DONE) { + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + module::set_var("gallery", "maintenance_mode", 0); + } else { + $task->percent_complete = round(100 * $completed / $total); + } + $task->status = t2("One operation complete", "%count / %total operations complete", $completed, + array("total" => $total)); + } + + static function find_dupe_slugs() { + return db::build() + ->select_distinct( + array("parent_slug" => db::expr("CONCAT(`parent_id`, ':', LOWER(`slug`))"))) + ->select("id") + ->select(array("C" => "COUNT(\"*\")")) + ->from("items") + ->having("C", ">", 1) + ->group_by("parent_slug") + ->execute(); + } + + static function find_dupe_names() { + // looking for photos, movies, and albums + return db::build() + ->select_distinct( + array("parent_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(`name`))"))) + ->select("id") + ->select(array("C" => "COUNT(\"*\")")) + ->from("items") + ->having("C", ">", 1) + ->group_by("parent_name") + ->execute(); + } + + static function find_dupe_base_names() { + // looking for photos or movies, not albums + return db::build() + ->select_distinct( + array("parent_base_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))"))) + ->select("id") + ->select(array("C" => "COUNT(\"*\")")) + ->from("items") + ->where("type", "<>", "album") + ->having("C", ">", 1) + ->group_by("parent_base_name") + ->execute(); + } + + static function find_empty_item_caches($limit) { + return db::build() + ->select("items.id") + ->from("items") + ->where("relative_path_cache", "is", null) + ->or_where("relative_url_cache", "is", null) + ->limit($limit) + ->execute(); + } + + static function find_missing_access_caches() { + return self::find_missing_access_caches_limited(1 << 16); + } + + static function find_missing_access_caches_limited($limit) { + return db::build() + ->select("items.id") + ->from("items") + ->join("access_caches", "items.id", "access_caches.item_id", "left") + ->where("access_caches.id", "is", null) + ->limit($limit) + ->execute(); + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/gallery_theme.php b/modules/gallery/helpers/gallery_theme.php new file mode 100644 index 0000000..3c6d71e --- /dev/null +++ b/modules/gallery/helpers/gallery_theme.php @@ -0,0 +1,151 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class gallery_theme_Core { + static function head($theme) { + $session = Session::instance(); + $buf = ""; + $buf .= $theme->css("gallery.css"); + if ($session->get("debug")) { + $buf .= $theme->css("debug.css"); + } + + if (module::is_active("rss")) { + if ($item = $theme->item()) { + if ($item->is_album()) { + $buf .= rss::feed_link("gallery/album/{$item->id}"); + } else { + $buf .= rss::feed_link("gallery/album/{$item->parent()->id}"); + } + } else if ($tag = $theme->tag()) { + $buf .= rss::feed_link("tag/tag/{$tag->id}"); + } + } + + if (count(locales::installed())) { + // Needed by the languages block + $buf .= $theme->script("jquery.cookie.js"); + } + + if ($session->get("l10n_mode", false)) { + $buf .= $theme->css("l10n_client.css") + . $theme->script("jquery.cookie.js") + . $theme->script("l10n_client.js"); + } + + $buf .= $theme->css("uploadify/uploadify.css"); + return $buf; + } + + static function admin_head($theme) { + $buf = $theme->css("gallery.css"); + $buf .= $theme->script("gallery.panel.js"); + $session = Session::instance(); + if ($session->get("debug")) { + $buf .= $theme->css("debug.css"); + } + + if ($session->get("l10n_mode", false)) { + $buf .= $theme->css("l10n_client.css"); + $buf .= $theme->script("jquery.cookie.js"); + $buf .= $theme->script("l10n_client.js"); + } + return $buf; + } + + static function page_bottom($theme) { + $session = Session::instance(); + if (gallery::show_profiler()) { + Profiler::enable(); + $profiler = new Profiler(); + $profiler->render(); + } + $content = ""; + if ($session->get("l10n_mode", false)) { + $content .= L10n_Client_Controller::l10n_form(); + } + + if ($session->get_once("after_install")) { + $content .= new View("welcome_message_loader.html"); + } + + if (identity::active_user()->admin && upgrade_checker::should_auto_check()) { + $content .= '<script type="text/javascript"> + $.ajax({url: "' . url::site("admin/upgrade_checker/check_now?csrf=" . + access::csrf_token()) . '"}); + </script>'; + } + return $content; + } + + static function admin_page_bottom($theme) { + $session = Session::instance(); + if (gallery::show_profiler()) { + Profiler::enable(); + $profiler = new Profiler(); + $profiler->render(); + } + + // Redirect to the root album when the admin session expires. + $content = '<script type="text/javascript"> + var adminReauthCheck = function() { + $.ajax({url: "' . url::site("admin?reauth_check=1") . '", + dataType: "json", + success: function(data){ + if ("location" in data) { + document.location = data.location; + } + }}); + }; + setInterval("adminReauthCheck();", 60 * 1000); + </script>'; + + if (upgrade_checker::should_auto_check()) { + $content .= '<script type="text/javascript"> + $.ajax({url: "' . url::site("admin/upgrade_checker/check_now?csrf=" . + access::csrf_token()) . '"}); + </script>'; + } + + if ($session->get("l10n_mode", false)) { + $content .= "\n" . L10n_Client_Controller::l10n_form(); + } + return $content; + } + + static function credits() { + $version_string = SafeString::of_safe_html( + '<bdo dir="ltr">Gallery ' . gallery::version_string() . '</bdo>'); + return "<li class=\"g-first\">" . + t(module::get_var("gallery", "credits"), + array("url" => "http://galleryproject.org", + "gallery_version" => $version_string)) . + "</li>"; + } + + static function admin_credits() { + return gallery_theme::credits(); + } + + static function body_attributes() { + if (locales::is_rtl()) { + return 'class="rtl"'; + } + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/graphics.php b/modules/gallery/helpers/graphics.php new file mode 100644 index 0000000..459784c --- /dev/null +++ b/modules/gallery/helpers/graphics.php @@ -0,0 +1,546 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class graphics_Core { + private static $init; + private static $_rules_cache = array(); + + /** + * Add a new graphics rule. + * + * Rules are applied to targets (thumbnails and resizes) in priority order. Rules are functions + * in the graphics class. So for example, the following rule: + * + * graphics::add_rule("gallery", "thumb", "gallery_graphics::resize", + * array("width" => 200, "height" => 200, "master" => Image::AUTO), 100); + * + * Specifies that "gallery" is adding a rule to resize thumbnails down to a max of 200px on + * the longest side. The gallery module adds default rules at a priority of 100. You can set + * higher and lower priorities to perform operations before or after this fires. + * + * @param string $module_name the module that added the rule + * @param string $target the target for this operation ("thumb" or "resize") + * @param string $operation the name of the operation (<defining class>::method) + * @param array $args arguments to the operation + * @param integer $priority the priority for this rule (lower priorities are run first) + */ + static function add_rule($module_name, $target, $operation, $args, $priority) { + $rule = ORM::factory("graphics_rule"); + $rule->module_name = $module_name; + $rule->target = $target; + $rule->operation = $operation; + $rule->priority = $priority; + $rule->args = serialize($args); + $rule->active = true; + $rule->save(); + + graphics::mark_dirty($target == "thumb", $target == "resize"); + } + + /** + * Remove any matching graphics rules + * @param string $module_name the module that added the rule + * @param string $target the target for this operation ("thumb" or "resize") + * @param string $operation the name of the operation(<defining class>::method) + */ + static function remove_rule($module_name, $target, $operation) { + db::build() + ->delete("graphics_rules") + ->where("module_name", "=", $module_name) + ->where("target", "=", $target) + ->where("operation", "=", $operation) + ->execute(); + + graphics::mark_dirty($target == "thumb", $target == "resize"); + } + + /** + * Remove all rules for this module + * @param string $module_name + */ + static function remove_rules($module_name) { + $status = db::build() + ->delete("graphics_rules") + ->where("module_name", "=", $module_name) + ->execute(); + if (count($status)) { + graphics::mark_dirty(true, true); + } + } + + /** + * Activate the rules for this module, typically done when the module itself is deactivated. + * Note that this does not mark images as dirty so that if you deactivate and reactivate a + * module it won't cause all of your images to suddenly require a rebuild. + */ + static function activate_rules($module_name) { + db::build() + ->update("graphics_rules") + ->set("active", true) + ->where("module_name", "=", $module_name) + ->execute(); + } + + /** + * Deactivate the rules for this module, typically done when the module itself is deactivated. + * Note that this does not mark images as dirty so that if you deactivate and reactivate a + * module it won't cause all of your images to suddenly require a rebuild. + */ + static function deactivate_rules($module_name) { + db::build() + ->update("graphics_rules") + ->set("active", false) + ->where("module_name", "=", $module_name) + ->execute(); + } + + /** + * Rebuild the thumb and resize for the given item. + * @param Item_Model $item + */ + static function generate($item) { + if ($item->thumb_dirty) { + $ops["thumb"] = $item->thumb_path(); + } + if ($item->resize_dirty && $item->is_photo()) { + $ops["resize"] = $item->resize_path(); + } + + try { + foreach ($ops as $target => $output_file) { + $working_file = $item->file_path(); + // Delete anything that might already be there + @unlink($output_file); + switch ($item->type) { + case "movie": + // Run movie_extract_frame events, which can either: + // - generate an output file, bypassing the ffmpeg-based movie::extract_frame + // - add to the options sent to movie::extract_frame (e.g. change frame extract time, + // add de-interlacing arguments to ffmpeg... see movie helper for more info) + // Note that the args are similar to those of the events in gallery_graphics + $movie_options_wrapper = new stdClass(); + $movie_options_wrapper->movie_options = array(); + module::event("movie_extract_frame", $working_file, $output_file, + $movie_options_wrapper, $item); + // If no output_file generated by events, run movie::extract_frame with movie_options + clearstatcache(); + if (@filesize($output_file) == 0) { + try { + movie::extract_frame($working_file, $output_file, $movie_options_wrapper->movie_options); + // If we're here, we know ffmpeg is installed and the movie is valid. Because the + // user may not always have had ffmpeg installed, the movie's width, height, and + // mime type may need updating. Let's use this opportunity to make sure they're + // correct. It's not optimal to do it at this low level, but it's not trivial to find + // these cases quickly in an upgrade script. + list ($width, $height, $mime_type) = movie::get_file_metadata($working_file); + // Only set them if they need updating to avoid marking them as "changed" + if (($item->width != $width) || ($item->height != $height) || + ($item->mime_type != $mime_type)) { + $item->width = $width; + $item->height = $height; + $item->mime_type = $mime_type; + } + } catch (Exception $e) { + // Didn't work, likely because of MISSING_FFMPEG - use placeholder + graphics::_replace_image_with_placeholder($item, $target); + break; + } + } + $working_file = $output_file; + + case "photo": + // Run the graphics rules (for both movies and photos) + foreach (self::_get_rules($target) as $rule) { + $args = array($working_file, $output_file, unserialize($rule->args), $item); + call_user_func_array($rule->operation, $args); + $working_file = $output_file; + } + break; + + case "album": + if (!$cover = $item->album_cover()) { + // This album has no cover; copy its placeholder image. Because of an old bug, it's + // possible that there's an album cover item id that points to an invalid item. In that + // case, just null out the album cover item id. It's not optimal to do that at this low + // level, but it's not trivial to find these cases quickly in an upgrade script and if we + // don't do this, the album may be permanently marked as "needs rebuilding" + // + // ref: http://sourceforge.net/apps/trac/gallery/ticket/1172 + // http://galleryproject.org/node/96926 + if ($item->album_cover_item_id) { + $item->album_cover_item_id = null; + $item->save(); + } + graphics::_replace_image_with_placeholder($item, $target); + break; + } + if ($cover->thumb_dirty) { + graphics::generate($cover); + } + if (!$cover->thumb_dirty) { + // Make the album cover from the cover item's thumb. Run gallery_graphics::resize with + // null options and it will figure out if this is a direct copy or conversion to jpg. + $working_file = $cover->thumb_path(); + gallery_graphics::resize($working_file, $output_file, null, $item); + } + break; + } + } + + if (!empty($ops["thumb"])) { + if (file_exists($item->thumb_path())) { + $item->thumb_dirty = 0; + } else { + Kohana_Log::add("error", "Failed to rebuild thumb image: $item->title"); + graphics::_replace_image_with_placeholder($item, "thumb"); + } + } + + if (!empty($ops["resize"])) { + if (file_exists($item->resize_path())) { + $item->resize_dirty = 0; + } else { + Kohana_Log::add("error", "Failed to rebuild resize image: $item->title"); + graphics::_replace_image_with_placeholder($item, "resize"); + } + } + graphics::_update_item_dimensions($item); + $item->save(); + } catch (Exception $e) { + // Something went wrong rebuilding the image. Replace with the placeholder images, + // leave it dirty and move on. + Kohana_Log::add("error", "Caught exception rebuilding images: {$item->title}\n" . + $e->getMessage() . "\n" . $e->getTraceAsString()); + if ($item->is_photo()) { + graphics::_replace_image_with_placeholder($item, "resize"); + } + graphics::_replace_image_with_placeholder($item, "thumb"); + try { + graphics::_update_item_dimensions($item); + } catch (Exception $e) { + // Looks like get_file_metadata couldn't identify our placeholders. We should never get + // here, but in the odd case we do, we need to do something. Let's put in hardcoded values. + if ($item->is_photo()) { + list ($item->resize_width, $item->resize_height) = array(200, 200); + } + list ($item->thumb_width, $item->thumb_height) = array(200, 200); + } + $item->save(); + throw $e; + } + } + + private static function _update_item_dimensions($item) { + if ($item->is_photo()) { + list ($item->resize_width, $item->resize_height) = + photo::get_file_metadata($item->resize_path()); + } + list ($item->thumb_width, $item->thumb_height) = + photo::get_file_metadata($item->thumb_path()); + } + + private static function _replace_image_with_placeholder($item, $target) { + if ($item->is_album() && !$item->album_cover_item_id) { + $input_path = MODPATH . "gallery/images/missing_album_cover.jpg"; + } else if ($item->is_movie() || ($item->is_album() && $item->album_cover()->is_movie())) { + $input_path = MODPATH . "gallery/images/missing_movie.jpg"; + } else { + $input_path = MODPATH . "gallery/images/missing_photo.jpg"; + } + + if ($target == "thumb") { + $output_path = $item->thumb_path(); + $size = module::get_var("gallery", "thumb_size", 200); + } else { + $output_path = $item->resize_path(); + $size = module::get_var("gallery", "resize_size", 640); + } + $options = array("width" => $size, "height" => $size, "master" => Image::AUTO); + + try { + // Copy/convert/resize placeholder as needed. + gallery_graphics::resize($input_path, $output_path, $options, null); + } catch (Exception $e) { + // Copy/convert/resize didn't work. Add to the log and copy the jpg version (which could have + // a non-jpg extension). This is less than ideal, but it's better than putting nothing + // there and causing theme views to act strangely because a file is missing. + // @todo we should handle this better. + Kohana_Log::add("error", "Caught exception converting placeholder for missing image: " . + $item->title . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); + copy($input_path, $output_path); + } + + if (!file_exists($output_path)) { + // Copy/convert/resize didn't throw an exception, but still didn't work - do the same as above. + // @todo we should handle this better. + Kohana_Log::add("error", "Failed to convert placeholder for missing image: $item->title"); + copy($input_path, $output_path); + } + } + + private static function _get_rules($target) { + if (empty(self::$_rules_cache[$target])) { + $rules = array(); + foreach (ORM::factory("graphics_rule") + ->where("target", "=", $target) + ->where("active", "=", true) + ->order_by("priority", "asc") + ->find_all() as $rule) { + $rules[] = (object)$rule->as_array(); + } + self::$_rules_cache[$target] = $rules; + } + return self::$_rules_cache[$target]; + } + + /** + * Return a query result that locates all items with dirty images. + * @return Database_Result Query result + */ + static function find_dirty_images_query() { + return db::build() + ->from("items") + ->and_open() + ->where("thumb_dirty", "=", 1) + ->and_open() + ->where("type", "<>", "album") + ->or_where("album_cover_item_id", "IS NOT", null) + ->close() + ->or_open() + ->where("resize_dirty", "=", 1) + ->where("type", "=", "photo") + ->close() + ->close(); + } + + /** + * Mark thumbnails and resizes as dirty. They will have to be rebuilt. Optionally, only those of + * a specified type and/or mime type can be marked (e.g. $type="movie" to rebuild movies only). + */ + static function mark_dirty($thumbs, $resizes, $type=null, $mime_type=null) { + if ($thumbs || $resizes) { + $db = db::build() + ->update("items"); + if ($type) { + $db->where("type", "=", $type); + } + if ($mime_type) { + $db->where("mime_type", "=", $mime_type); + } + if ($thumbs) { + $db->set("thumb_dirty", 1); + } + if ($resizes) { + $db->set("resize_dirty", 1); + } + $db->execute(); + } + + $count = graphics::find_dirty_images_query()->count_records(); + if ($count) { + site_status::warning( + t2("One of your photos is out of date. <a %attrs>Click here to fix it</a>", + "%count of your photos are out of date. <a %attrs>Click here to fix them</a>", + $count, + array("attrs" => html::mark_clean(sprintf( + 'href="%s" class="g-dialog-link"', + url::site("admin/maintenance/start/gallery_task::rebuild_dirty_images?csrf=__CSRF__"))))), + "graphics_dirty"); + } + } + + /** + * Detect which graphics toolkits are available on this system. Return an array of key value + * pairs where the key is one of gd, imagemagick, graphicsmagick and the value is information + * about that toolkit. For GD we return the version string, and for ImageMagick and + * GraphicsMagick we return the path to the directory containing the appropriate binaries. + */ + static function detect_toolkits() { + $toolkits = new stdClass(); + $toolkits->gd = new stdClass(); + $toolkits->imagemagick = new stdClass(); + $toolkits->graphicsmagick = new stdClass(); + + // GD is special, it doesn't use exec() + $gd = function_exists("gd_info") ? gd_info() : array(); + $toolkits->gd->name = "GD"; + if (!isset($gd["GD Version"])) { + $toolkits->gd->installed = false; + $toolkits->gd->error = t("GD is not installed"); + } else { + $toolkits->gd->installed = true; + $toolkits->gd->version = $gd["GD Version"]; + $toolkits->gd->rotate = function_exists("imagerotate"); + $toolkits->gd->sharpen = function_exists("imageconvolution"); + $toolkits->gd->binary = ""; + $toolkits->gd->dir = ""; + + if (!$toolkits->gd->rotate && !$toolkits->gd->sharpen) { + $toolkits->gd->error = + t("You have GD version %version, but it lacks image rotation and sharpening.", + array("version" => $gd["GD Version"])); + } else if (!$toolkits->gd->rotate) { + $toolkits->gd->error = + t("You have GD version %version, but it lacks image rotation.", + array("version" => $gd["GD Version"])); + } else if (!$toolkits->gd->sharpen) { + $toolkits->gd->error = + t("You have GD version %version, but it lacks image sharpening.", + array("version" => $gd["GD Version"])); + } + } + + if (!function_exists("exec")) { + $toolkits->imagemagick->installed = false; + $toolkits->imagemagick->error = t("ImageMagick requires the <b>exec</b> function"); + + $toolkits->graphicsmagick->installed = false; + $toolkits->graphicsmagick->error = t("GraphicsMagick requires the <b>exec</b> function"); + } else { + // ImageMagick & GraphicsMagick + $magick_kits = array( + "imagemagick" => array( + "name" => "ImageMagick", "binary" => "convert", "version_arg" => "-version", + "version_regex" => "/Version: \S+ (\S+)/"), + "graphicsmagick" => array( + "name" => "GraphicsMagick", "binary" => "gm", "version_arg" => "version", + "version_regex" => "/\S+ (\S+)/")); + // Loop through the kits + foreach ($magick_kits as $index => $settings) { + $path = system::find_binary( + $settings["binary"], module::get_var("gallery", "graphics_toolkit_path")); + $toolkits->$index->name = $settings["name"]; + if ($path) { + if (@is_file($path) && + preg_match( + $settings["version_regex"], shell_exec($path . " " . $settings["version_arg"]), $matches)) { + $version = $matches[1]; + + $toolkits->$index->installed = true; + $toolkits->$index->version = $version; + $toolkits->$index->binary = $path; + $toolkits->$index->dir = dirname($path); + $toolkits->$index->rotate = true; + $toolkits->$index->sharpen = true; + } else { + $toolkits->$index->installed = false; + $toolkits->$index->error = + t("%toolkit_name is installed, but PHP's open_basedir restriction prevents Gallery from using it.", + array("toolkit_name" => $settings["name"])); + } + } else { + $toolkits->$index->installed = false; + $toolkits->$index->error = + t("We could not locate %toolkit_name on your system.", + array("toolkit_name" => $settings["name"])); + } + } + } + + return $toolkits; + } + + /** + * This needs to be run once, after the initial install, to choose a graphics toolkit. + */ + static function choose_default_toolkit() { + // Detect a graphics toolkit + $toolkits = graphics::detect_toolkits(); + foreach (array("imagemagick", "graphicsmagick", "gd") as $tk) { + if ($toolkits->$tk->installed) { + module::set_var("gallery", "graphics_toolkit", $tk); + module::set_var("gallery", "graphics_toolkit_path", $toolkits->$tk->dir); + break; + } + } + + if (!module::get_var("gallery", "graphics_toolkit")) { + site_status::warning( + t("Graphics toolkit missing! Please <a href=\"%url\">choose a toolkit</a>", + array("url" => html::mark_clean(url::site("admin/graphics")))), + "missing_graphics_toolkit"); + } + } + + /** + * Choose which driver the Kohana Image library uses. + */ + static function init_toolkit() { + if (self::$init) { + return; + } + switch(module::get_var("gallery", "graphics_toolkit")) { + case "gd": + Kohana_Config::instance()->set("image.driver", "GD"); + break; + + case "imagemagick": + Kohana_Config::instance()->set("image.driver", "ImageMagick"); + Kohana_Config::instance()->set( + "image.params.directory", module::get_var("gallery", "graphics_toolkit_path")); + break; + + case "graphicsmagick": + Kohana_Config::instance()->set("image.driver", "GraphicsMagick"); + Kohana_Config::instance()->set( + "image.params.directory", module::get_var("gallery", "graphics_toolkit_path")); + break; + } + + self::$init = 1; + } + + /** + * Verify that a specific graphics function is available with the active toolkit. + * @param string $func (eg rotate, sharpen) + * @return boolean + */ + static function can($func) { + if (module::get_var("gallery", "graphics_toolkit") == "gd") { + switch ($func) { + case "rotate": + return function_exists("imagerotate"); + + case "sharpen": + return function_exists("imageconvolution"); + } + } + + return true; + } + + /** + * Return the max file size that this graphics toolkit can handle. + */ + static function max_filesize() { + if (module::get_var("gallery", "graphics_toolkit") == "gd") { + $memory_limit = trim(ini_get("memory_limit")); + $memory_limit_bytes = num::convert_to_bytes($memory_limit); + + // GD expands images in memory and uses 4 bytes of RAM for every byte + // in the file. + $max_filesize = $memory_limit_bytes / 4; + $max_filesize_human_readable = num::convert_to_human_readable($max_filesize); + return array($max_filesize, $max_filesize_human_readable); + } + + // Some arbitrarily large size + return array(1000000000, "1G"); + } +} diff --git a/modules/gallery/helpers/identity.php b/modules/gallery/helpers/identity.php new file mode 100644 index 0000000..5fc0f2f --- /dev/null +++ b/modules/gallery/helpers/identity.php @@ -0,0 +1,247 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class identity_Core { + protected static $available; + + /** + * Return a list of installed Identity Drivers. + * + * @return boolean true if the driver supports updates; false if read only + */ + static function providers() { + if (empty(self::$available)) { + $drivers = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS); + foreach (module::available() as $module_name => $module) { + if (file_exists(MODPATH . "{$module_name}/config/identity.php")) { + $drivers->$module_name = $module->description; + } + } + self::$available = $drivers; + } + return self::$available; + } + + /** + * Frees the current instance of the identity provider so the next call to instance will reload + * + * @param string configuration + * @return Identity_Core + */ + static function reset() { + IdentityProvider::reset(); + } + + /** + * Make sure that we have a session and group_ids cached in the session. + */ + static function load_user() { + try { + // Call IdentityProvider::instance() now to force the load of the user interface classes. + // We are about to load the active user from the session and which needs the user definition + // class, which can't be reached by Kohana's heiracrchical lookup. + IdentityProvider::instance(); + + $session = Session::instance(); + if (!($user = $session->get("user"))) { + identity::set_active_user($user = identity::guest()); + } + + // The installer cannot set a user into the session, so it just sets an id which we should + // upconvert into a user. + // @todo set the user name into the session instead of 2 and then use it to get the + // user object + if ($user === 2) { + $session->delete("user"); // delete it so that identity code isn't confused by the integer + auth::login(IdentityProvider::instance()->admin_user()); + } + + // Cache the group ids for a day to trade off performance for security updates. + if (!$session->get("group_ids") || $session->get("group_ids_timeout", 0) < time()) { + $ids = array(); + foreach ($user->groups() as $group) { + $ids[] = $group->id; + } + $session->set("group_ids", $ids); + $session->set("group_ids_timeout", time() + 86400); + } + } catch (Exception $e) { + // Log it, so we at least have so notification that we swallowed the exception. + Kohana_Log::add("error", "load_user Exception: " . + $e->getMessage() . "\n" . $e->getTraceAsString()); + try { + Session::instance()->destroy(); + } catch (Exception $e) { + // We don't care if there was a problem destroying the session. + } + url::redirect(item::root()->abs_url()); + } + } + + /** + * Return the array of group ids this user belongs to + * + * @return array + */ + static function group_ids_for_active_user() { + return Session::instance()->get("group_ids", array(1)); + } + + /** + * Return the active user. If there's no active user, return the guest user. + * + * @return User_Definition + */ + static function active_user() { + // @todo (maybe) cache this object so we're not always doing session lookups. + $user = Session::instance()->get("user", null); + if (!isset($user)) { + // Don't do this as a fallback in the Session::get() call because it can trigger unnecessary + // work. + $user = identity::guest(); + } + return $user; + } + + /** + * Change the active user. + * @param User_Definition $user + */ + static function set_active_user($user) { + $session = Session::instance(); + $session->set("user", $user); + $session->delete("group_ids"); + identity::load_user(); + } + + /** + * Determine if if the current driver supports updates. + * + * @return boolean true if the driver supports updates; false if read only + */ + static function is_writable() { + return IdentityProvider::instance()->is_writable(); + } + + /** + * @see IdentityProvider_Driver::guest. + */ + static function guest() { + return IdentityProvider::instance()->guest(); + } + + /** + * @see IdentityProvider_Driver::admin_user. + */ + static function admin_user() { + return IdentityProvider::instance()->admin_user(); + } + + /** + * @see IdentityProvider_Driver::create_user. + */ + static function create_user($name, $full_name, $password, $email) { + return IdentityProvider::instance()->create_user($name, $full_name, $password, $email); + } + + /** + * @see IdentityProvider_Driver::is_correct_password. + */ + static function is_correct_password($user, $password) { + return IdentityProvider::instance()->is_correct_password($user, $password); + } + + /** + * @see IdentityProvider_Driver::lookup_user. + */ + static function lookup_user($id) { + return IdentityProvider::instance()->lookup_user($id); + } + + /** + * @see IdentityProvider_Driver::lookup_user_by_name. + */ + static function lookup_user_by_name($name) { + return IdentityProvider::instance()->lookup_user_by_name($name); + } + + /** + * @see IdentityProvider_Driver::create_group. + */ + static function create_group($name) { + return IdentityProvider::instance()->create_group($name); + } + + /** + * @see IdentityProvider_Driver::everybody. + */ + static function everybody() { + return IdentityProvider::instance()->everybody(); + } + + /** + * @see IdentityProvider_Driver::registered_users. + */ + static function registered_users() { + return IdentityProvider::instance()->registered_users(); + } + + /** + * @see IdentityProvider_Driver::lookup_group. + */ + static function lookup_group($id) { + return IdentityProvider::instance()->lookup_group($id); + } + + /** + * @see IdentityProvider_Driver::lookup_group_by_name. + */ + static function lookup_group_by_name($name) { + return IdentityProvider::instance()->lookup_group_by_name($name); + } + + /** + * @see IdentityProvider_Driver::get_user_list. + */ + static function get_user_list($ids) { + return IdentityProvider::instance()->get_user_list($ids); + } + + /** + * @see IdentityProvider_Driver::groups. + */ + static function groups() { + return IdentityProvider::instance()->groups(); + } + + /** + * @see IdentityProvider_Driver::add_user_to_group. + */ + static function add_user_to_group($user, $group) { + return IdentityProvider::instance()->add_user_to_group($user, $group); + } + + /** + * @see IdentityProvider_Driver::remove_user_to_group. + */ + static function remove_user_from_group($user, $group) { + return IdentityProvider::instance()->remove_user_from_group($user, $group); + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php new file mode 100644 index 0000000..9882a9c --- /dev/null +++ b/modules/gallery/helpers/item.php @@ -0,0 +1,433 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class item_Core { + static function move($source, $target) { + access::required("view", $source); + access::required("view", $target); + access::required("edit", $source); + access::required("edit", $target); + + $parent = $source->parent(); + if ($parent->album_cover_item_id == $source->id) { + if ($parent->children_count() > 1) { + foreach ($parent->children(2) as $child) { + if ($child->id != $source->id) { + $new_cover_item = $child; + break; + } + } + item::make_album_cover($new_cover_item); + } else { + item::remove_album_cover($parent); + } + } + + $orig_name = $source->name; + $source->parent_id = $target->id; + $source->save(); + if ($orig_name != $source->name) { + switch ($source->type) { + case "album": + message::info( + t("Album <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "photo": + message::info( + t("Photo <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + + case "movie": + message::info( + t("Movie <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict", + array("old_name" => $orig_name, "new_name" => $source->name))); + break; + } + } + + // If the target has no cover item, make this it. + if ($target->album_cover_item_id == null) { + item::make_album_cover($source); + } + } + + static function make_album_cover($item) { + $parent = $item->parent(); + access::required("view", $item); + access::required("view", $parent); + access::required("edit", $parent); + + $old_album_cover_id = $parent->album_cover_item_id; + + model_cache::clear(); + $parent->album_cover_item_id = $item->is_album() ? $item->album_cover_item_id : $item->id; + $parent->save(); + graphics::generate($parent); + + // Walk up the parent hierarchy and set album covers if necessary + $grand_parent = $parent->parent(); + if ($grand_parent && access::can("edit", $grand_parent) && + $grand_parent->album_cover_item_id == null) { + item::make_album_cover($parent); + } + + // When albums are album covers themselves, we hotlink directly to the target item. This + // means that when we change an album cover, the grandparent may have a deep link to the old + // album cover. So find any parent albums that had the old item as their album cover and + // switch them over to the new item. + if ($old_album_cover_id) { + foreach ($item->parents(array(array("album_cover_item_id", "=", $old_album_cover_id))) + as $ancestor) { + if (access::can("edit", $ancestor)) { + $ancestor->album_cover_item_id = $parent->album_cover_item_id; + $ancestor->save(); + graphics::generate($ancestor); + } + } + } + } + + static function remove_album_cover($album) { + access::required("view", $album); + access::required("edit", $album); + + model_cache::clear(); + $album->album_cover_item_id = null; + $album->save(); + graphics::generate($album); + } + + /** + * Sanitize a filename into something presentable as an item title + * @param string $filename + * @return string title + */ + static function convert_filename_to_title($filename) { + $title = strtr($filename, "_", " "); + $title = preg_replace("/\..{3,4}$/", "", $title); + $title = preg_replace("/ +/", " ", $title); + return $title; + } + + /** + * Convert a filename into something we can use as a url component. + * @param string $filename + */ + static function convert_filename_to_slug($filename) { + $result = str_replace("&", "-and-", $filename); + $result = str_replace(" ", "-", $result); + + // It's not easy to extend the text helper since it's called by the Input class which is + // referenced in hooks/init_gallery, so it's + if (class_exists("transliterate")) { + $result = transliterate::utf8_to_ascii($result); + } else { + $result = text::transliterate_to_ascii($result); + } + $result = preg_replace("/[^A-Za-z0-9-_]+/", "-", $result); + $result = preg_replace("/-+/", "-", $result); + return trim($result, "-"); + } + + /** + * Display delete confirmation message and form + * @param object $item + * @return string form + */ + static function get_delete_form($item) { + $page_type = Input::instance()->get("page_type"); + $from_id = Input::instance()->get("from_id"); + $form = new Forge( + "quick/delete/$item->id?page_type=$page_type&from_id=$from_id", "", + "post", array("id" => "g-confirm-delete")); + $group = $form->group("confirm_delete")->label(t("Confirm Deletion")); + $group->submit("")->value(t("Delete")); + $form->script("") + ->url(url::abs_file("modules/gallery/js/item_form_delete.js")); + return $form; + } + + /** + * Get the next weight value + */ + static function get_max_weight() { + // Guard against an empty result when we create the first item. It's unfortunate that we + // have to check this every time. + // @todo: figure out a better way to bootstrap the weight. + $result = db::build() + ->select("weight")->from("items") + ->order_by("weight", "desc")->limit(1) + ->execute()->current(); + return ($result ? $result->weight : 0) + 1; + } + + /** + * Add a set of restrictions to any following queries to restrict access only to items + * viewable by the active user. + * @chainable + */ + static function viewable($model) { + $view_restrictions = array(); + if (!identity::active_user()->admin) { + foreach (identity::group_ids_for_active_user() as $id) { + $view_restrictions[] = array("items.view_$id", "=", access::ALLOW); + } + } + + if (count($view_restrictions)) { + $model->and_open()->merge_or_where($view_restrictions)->close(); + } + + return $model; + } + + /** + * Find an item by its path. If there's no match, return an empty Item_Model. + * NOTE: the caller is responsible for performing security checks on the resulting item. + * @param string $path + * @return object Item_Model + */ + static function find_by_path($path) { + $path = trim($path, "/"); + + // The root path name is NULL not "", hence this workaround. + if ($path == "") { + return item::root(); + } + + // Check to see if there's an item in the database with a matching relative_path_cache value. + // Since that field is urlencoded, we must urlencoded the components of the path. + foreach (explode("/", $path) as $part) { + $encoded_array[] = rawurlencode($part); + } + $encoded_path = join("/", $encoded_array); + $item = ORM::factory("item") + ->where("relative_path_cache", "=", $encoded_path) + ->find(); + if ($item->loaded()) { + return $item; + } + + // Since the relative_path_cache field is a cache, it can be unavailable. If we don't find + // anything, fall back to checking the path the hard way. + $paths = explode("/", $path); + foreach (ORM::factory("item") + ->where("name", "=", end($paths)) + ->where("level", "=", count($paths) + 1) + ->find_all() as $item) { + if (urldecode($item->relative_path()) == $path) { + return $item; + } + } + + return new Item_Model(); + } + + + /** + * Locate an item using the URL. We assume that the url is in the form /a/b/c where each + * component matches up with an item slug. If there's no match, return an empty Item_Model + * NOTE: the caller is responsible for performing security checks on the resulting item. + * @param string $url the relative url fragment + * @return Item_Model + */ + static function find_by_relative_url($relative_url) { + // In most cases, we'll have an exact match in the relative_url_cache item field. + // but failing that, walk down the tree until we find it. The fallback code will fix caches + // as it goes, so it'll never be run frequently. + $item = ORM::factory("item")->where("relative_url_cache", "=", $relative_url)->find(); + if (!$item->loaded()) { + $segments = explode("/", $relative_url); + foreach (ORM::factory("item") + ->where("slug", "=", end($segments)) + ->where("level", "=", count($segments) + 1) + ->find_all() as $match) { + if ($match->relative_url() == $relative_url) { + $item = $match; + } + } + } + return $item; + } + + /** + * Return the root Item_Model + * @return Item_Model + */ + static function root() { + return model_cache::get("item", 1); + } + + /** + * Return a query to get a random Item_Model, with optional filters. + * Usage: item::random_query()->execute(); + * + * Note: You can add your own ->where() clauses but if your Gallery is + * small or your where clauses are over-constrained you may wind up with + * no item. You should try running this a few times in a loop if you + * don't get an item back. + */ + static function random_query() { + // Pick a random number and find the item that's got nearest smaller number. + // This approach works best when the random numbers in the system are roughly evenly + // distributed so this is going to be more efficient with larger data sets. + return ORM::factory("item") + ->viewable() + ->where("rand_key", "<", random::percent()) + ->order_by("rand_key", "DESC"); + } + + /** + * Find the position of the given item in its parent album. The resulting + * value is 1-indexed, so the first child in the album is at position 1. + * + * @param Item_Model $item + * @param array $where an array of arrays, each compatible with ORM::where() + */ + static function get_position($item, $where=array()) { + $album = $item->parent(); + + if (!strcasecmp($album->sort_order, "DESC")) { + $comp = ">"; + } else { + $comp = "<"; + } + $query_model = ORM::factory("item"); + + // If the comparison column has NULLs in it, we can't use comparators on it + // and will have to deal with it the hard way. + $count = $query_model->viewable() + ->where("parent_id", "=", $album->id) + ->where($album->sort_column, "IS", null) + ->merge_where($where) + ->count_all(); + + if (empty($count)) { + // There are no NULLs in the sort column, so we can just use it directly. + $sort_column = $album->sort_column; + + $position = $query_model->viewable() + ->where("parent_id", "=", $album->id) + ->where($sort_column, $comp, $item->$sort_column) + ->merge_where($where) + ->count_all(); + + // We stopped short of our target value in the sort (notice that we're + // using a inequality comparator above) because it's possible that we have + // duplicate values in the sort column. An equality check would just + // arbitrarily pick one of those multiple possible equivalent columns, + // which would mean that if you choose a sort order that has duplicates, + // it'd pick any one of them as the child's "position". + // + // Fix this by doing a 2nd query where we iterate over the equivalent + // columns and add them to our position count. + foreach ($query_model->viewable() + ->select("id") + ->where("parent_id", "=", $album->id) + ->where($sort_column, "=", $item->$sort_column) + ->merge_where($where) + ->order_by(array("id" => "ASC")) + ->find_all() as $row) { + $position++; + if ($row->id == $item->id) { + break; + } + } + } else { + // There are NULLs in the sort column, so we can't use MySQL comparators. + // Fall back to iterating over every child row to get to the current one. + // This can be wildly inefficient for really large albums, but it should + // be a rare case that the user is sorting an album with null values in + // the sort column. + // + // Reproduce the children() functionality here using Database directly to + // avoid loading the whole ORM for each row. + $order_by = array($album->sort_column => $album->sort_order); + // Use id as a tie breaker + if ($album->sort_column != "id") { + $order_by["id"] = "ASC"; + } + + $position = 0; + foreach ($query_model->viewable() + ->select("id") + ->where("parent_id", "=", $album->id) + ->merge_where($where) + ->order_by($order_by) + ->find_all() as $row) { + $position++; + if ($row->id == $item->id) { + break; + } + } + } + + return $position; + } + + /** + * Set the display context callback for any future item renders. + */ + static function set_display_context_callback() { + if (!request::user_agent("robot")) { + $args = func_get_args(); + Cache::instance()->set("display_context_" . $sid = Session::instance()->id(), $args, + array("display_context")); + } + } + + /** + * Get rid of the display context callback + */ + static function clear_display_context_callback() { + Cache::instance()->delete("display_context_" . $sid = Session::instance()->id()); + } + + /** + * Call the display context callback for the given item + */ + static function get_display_context($item) { + if (!request::user_agent("robot")) { + $args = Cache::instance()->get("display_context_" . $sid = Session::instance()->id()); + $callback = $args[0]; + $args[0] = $item; + } + + if (empty($callback)) { + $callback = "Albums_Controller::get_display_context"; + $args = array($item); + } + return call_user_func_array($callback, $args); + } + + /** + * Reset all child weights of a given album to a monotonically increasing sequence based on the + * current sort order of the album. + */ + static function resequence_child_weights($album) { + $weight = 0; + foreach ($album->children() as $child) { + $child->weight = ++$weight; + $child->save(); + } + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/item_rest.php b/modules/gallery/helpers/item_rest.php new file mode 100644 index 0000000..efeba2e --- /dev/null +++ b/modules/gallery/helpers/item_rest.php @@ -0,0 +1,210 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class item_rest_Core { + /** + * For items that are collections, you can specify the following additional query parameters to + * query the collection. You can specify them in any combination. + * + * scope=direct + * Only return items that are immediately under this one + * scope=all + * Return items anywhere under this one + * + * name=<substring> + * Only return items where the name contains this substring + * + * random=true + * Return a single random item + * + * type=<comma separate list of photo, movie or album> + * Limit the type to types in this list, eg: "type=photo,movie". + * Also limits the types returned in the member collections (same behaviour as item_rest). + */ + static function get($request) { + $item = rest::resolve($request->url); + access::required("view", $item); + + $p = $request->params; + if (isset($p->random)) { + $orm = item::random_query()->offset(0)->limit(1); + } else { + $orm = ORM::factory("item")->viewable(); + } + + if (empty($p->scope)) { + $p->scope = "direct"; + } + + if (!in_array($p->scope, array("direct", "all"))) { + throw new Rest_Exception("Bad Request", 400); + } + + if ($p->scope == "direct") { + $orm->where("parent_id", "=", $item->id); + } else { + $orm->where("left_ptr", ">", $item->left_ptr); + $orm->where("right_ptr", "<", $item->right_ptr); + } + + if (isset($p->name)) { + $orm->where("name", "LIKE", "%" . Database::escape_for_like($p->name) . "%"); + } + + if (isset($p->type)) { + $orm->where("type", "IN", explode(",", $p->type)); + } + + // Apply the item's sort order, using id as the tie breaker. + // See Item_Model::children() + $order_by = array($item->sort_column => $item->sort_order); + if ($item->sort_column != "id") { + $order_by["id"] = "ASC"; + } + $orm->order_by($order_by); + + $result = array( + "url" => $request->url, + "entity" => $item->as_restful_array(), + "relationships" => rest::relationships("item", $item)); + if ($item->is_album()) { + $result["members"] = array(); + foreach ($orm->find_all() as $child) { + $result["members"][] = rest::url("item", $child); + } + } + + return $result; + } + + static function put($request) { + $item = rest::resolve($request->url); + access::required("edit", $item); + + if ($entity = $request->params->entity) { + // Only change fields from a whitelist. + foreach (array("album_cover", "captured", "description", + "height", "mime_type", "name", "parent", "rand_key", "resize_dirty", + "resize_height", "resize_width", "slug", "sort_column", "sort_order", + "thumb_dirty", "thumb_height", "thumb_width", "title", "view_count", + "width") as $key) { + switch ($key) { + case "album_cover": + if (property_exists($entity, "album_cover")) { + $album_cover_item = rest::resolve($entity->album_cover); + access::required("view", $album_cover_item); + $item->album_cover_item_id = $album_cover_item->id; + } + break; + + case "parent": + if (property_exists($entity, "parent")) { + $parent = rest::resolve($entity->parent); + access::required("edit", $parent); + $item->parent_id = $parent->id; + } + break; + default: + if (property_exists($entity, $key)) { + $item->$key = $entity->$key; + } + } + } + } + + // Replace the data file, if required + if (($item->is_photo() || $item->is_movie()) && isset($request->file)) { + $item->set_data_file($request->file); + } + + $item->save(); + + if (isset($request->params->members) && $item->sort_column == "weight") { + $weight = 0; + foreach ($request->params->members as $url) { + $child = rest::resolve($url); + if ($child->parent_id == $item->id && $child->weight != $weight) { + $child->weight = $weight; + $child->save(); + } + $weight++; + } + } + } + + static function post($request) { + $parent = rest::resolve($request->url); + access::required("add", $parent); + + $entity = $request->params->entity; + $item = ORM::factory("item"); + switch ($entity->type) { + case "album": + $item->type = "album"; + $item->parent_id = $parent->id; + $item->name = $entity->name; + $item->title = isset($entity->title) ? $entity->title : $entity->name; + $item->description = isset($entity->description) ? $entity->description : null; + $item->slug = isset($entity->slug) ? $entity->slug : null; + $item->save(); + break; + + case "photo": + case "movie": + if (empty($request->file)) { + throw new Rest_Exception( + "Bad Request", 400, array("errors" => array("file" => t("Upload failed")))); + } + $item->type = $entity->type; + $item->parent_id = $parent->id; + $item->set_data_file($request->file); + $item->name = $entity->name; + $item->title = isset($entity->title) ? $entity->title : $entity->name; + $item->description = isset($entity->description) ? $entity->description : null; + $item->slug = isset($entity->slug) ? $entity->slug : null; + $item->save(); + break; + + default: + throw new Rest_Exception( + "Bad Request", 400, array("errors" => array("type" => "invalid"))); + } + + return array("url" => rest::url("item", $item)); + } + + static function delete($request) { + $item = rest::resolve($request->url); + access::required("edit", $item); + + $item->delete(); + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + return $item; + } + + static function url($item) { + return url::abs_site("rest/item/{$item->id}"); + } +} diff --git a/modules/gallery/helpers/items_rest.php b/modules/gallery/helpers/items_rest.php new file mode 100644 index 0000000..7622c53 --- /dev/null +++ b/modules/gallery/helpers/items_rest.php @@ -0,0 +1,96 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class items_rest_Core { + /** + * To retrieve a collection of items, you can specify the following query parameters to specify + * the type of the collection. If both are specified, then the url parameter is used and the + * ancestors_for is ignored. Specifying the "type" parameter with the urls parameter, will + * filter the results based on the specified type. Using the type parameter with the + * ancestors_for parameter makes no sense and will be ignored. + * + * urls=["url1","url2","url3"] + * Return items that match the specified urls. Typically used to return the member detail + * + * ancestors_for=url + * Return the ancestors of the specified item + * + * type=<comma separate list of photo, movie or album> + * Limit the type to types in this list, eg: "type=photo,movie". + * Also limits the types returned in the member collections (same behaviour as item_rest). + * Ignored if ancestors_for is set. + */ + static function get($request) { + $items = array(); + $types = array(); + + if (isset($request->params->urls)) { + if (isset($request->params->type)) { + $types = explode(",", $request->params->type); + } + + foreach (json_decode($request->params->urls) as $url) { + $item = rest::resolve($url); + if (!access::can("view", $item)) { + continue; + } + + if (empty($types) || in_array($item->type, $types)) { + $items[] = items_rest::_format_restful_item($item, $types); + } + } + } else if (isset($request->params->ancestors_for)) { + $item = rest::resolve($request->params->ancestors_for); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + $items[] = items_rest::_format_restful_item($item, $types); + while (($item = $item->parent()) != null) { + array_unshift($items, items_rest::_format_restful_item($item, $types)); + }; + } + + return $items; + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + return $item; + } + + private static function _format_restful_item($item, $types) { + $item_rest = array("url" => rest::url("item", $item), + "entity" => $item->as_restful_array(), + "relationships" => rest::relationships("item", $item)); + if ($item->type == "album") { + $members = array(); + foreach ($item->viewable()->children() as $child) { + if (empty($types) || in_array($child->type, $types)) { + $members[] = rest::url("item", $child); + } + } + $item_rest["members"] = $members; + } + + return $item_rest; + } +} diff --git a/modules/gallery/helpers/json.php b/modules/gallery/helpers/json.php new file mode 100644 index 0000000..b56f269 --- /dev/null +++ b/modules/gallery/helpers/json.php @@ -0,0 +1,31 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class json_Core { + /** + * JSON Encode a reply to the browser and set the content type to specify that it's a JSON + * payload. + * + * @param mixed $message string or object to json encode and print + */ + static function reply($message) { + header("Content-Type: application/json; charset=" . Kohana::CHARSET); + print json_encode($message); + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/l10n_client.php b/modules/gallery/helpers/l10n_client.php new file mode 100644 index 0000000..2a1be2f --- /dev/null +++ b/modules/gallery/helpers/l10n_client.php @@ -0,0 +1,323 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class l10n_client_Core { + + private static function _server_url($path) { + return "http://galleryproject.org/translations/$path"; + } + + static function server_api_key_url() { + return self::_server_url("userkey/" . self::client_token()); + } + + static function client_token() { + return md5("l10n_client_client_token" . access::private_key()); + } + + static function api_key($api_key=null) { + if ($api_key !== null) { + module::set_var("gallery", "l10n_client_key", $api_key); + } + return module::get_var("gallery", "l10n_client_key", ""); + } + + static function server_uid($api_key=null) { + $api_key = $api_key == null ? l10n_client::api_key() : $api_key; + $parts = explode(":", $api_key); + return empty($parts) ? 0 : $parts[0]; + } + + private static function _sign($payload, $api_key=null) { + $api_key = $api_key == null ? l10n_client::api_key() : $api_key; + return md5($api_key . $payload . l10n_client::client_token()); + } + + static function validate_api_key($api_key) { + $version = "1.0"; + $url = self::_server_url("status"); + $signature = self::_sign($version, $api_key); + + try { + list ($response_data, $response_status) = remote::post( + $url, array("version" => $version, + "client_token" => l10n_client::client_token(), + "signature" => $signature, + "uid" => l10n_client::server_uid($api_key))); + } catch (ErrorException $e) { + // Log the error, but then return a "can't make connection" error + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + if (!isset($response_data) && !isset($response_status)) { + return array(false, false); + } + + if (!remote::success($response_status)) { + return array(true, false); + } + return array(true, true); + } + + /** + * Fetches translations for l10n messages. Must be called repeatedly + * until 0 is returned (which is a countdown indicating progress). + * + * @param $num_fetched in/out parameter to specify which batch of + * messages to fetch translations for. + * @return The number of messages for which we didn't fetch + * translations for. + */ + static function fetch_updates(&$num_fetched) { + $request = new stdClass(); + $request->locales = array(); + $request->messages = new stdClass(); + + $locales = locales::installed(); + foreach ($locales as $locale => $locale_data) { + $request->locales[] = $locale; + } + + // See the server side code for how we arrive at this + // number as a good limit for #locales * #messages. + $max_messages = 2000 / count($locales); + $num_messages = 0; + $rows = db::build() + ->select("key", "locale", "revision", "translation") + ->from("incoming_translations") + ->order_by("key") + ->limit(1000000) // ignore, just there to satisfy SQL syntax + ->offset($num_fetched) + ->execute(); + $num_remaining = $rows->count(); + foreach ($rows as $row) { + if (!isset($request->messages->{$row->key})) { + if ($num_messages >= $max_messages) { + break; + } + $request->messages->{$row->key} = 1; + $num_messages++; + } + if (!empty($row->revision) && !empty($row->translation) && + isset($locales[$row->locale])) { + if (!is_object($request->messages->{$row->key})) { + $request->messages->{$row->key} = new stdClass(); + } + $request->messages->{$row->key}->{$row->locale} = (int) $row->revision; + } + $num_fetched++; + $num_remaining--; + } + // @todo Include messages from outgoing_translations? + + if (!$num_messages) { + return $num_remaining; + } + + $request_data = json_encode($request); + $url = self::_server_url("fetch"); + list ($response_data, $response_status) = remote::post($url, array("data" => $request_data)); + if (!remote::success($response_status)) { + throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED " . $response_status); + } + if (empty($response_data)) { + return $num_remaining; + } + + $response = json_decode($response_data); + + // Response format (JSON payload): + // [{key:<key_1>, translation: <JSON encoded translation>, rev:<rev>, locale:<locale>}, + // {key:<key_2>, ...} + // ] + foreach ($response as $message_data) { + // @todo Better input validation + if (empty($message_data->key) || empty($message_data->translation) || + empty($message_data->locale) || empty($message_data->rev)) { + throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED: Invalid response data"); + } + $key = $message_data->key; + $locale = $message_data->locale; + $revision = $message_data->rev; + $translation = json_decode($message_data->translation); + if (!is_string($translation)) { + // Normalize stdclass to array + $translation = (array) $translation; + } + $translation = serialize($translation); + + // @todo Should we normalize the incoming_translations table into messages(id, key, message) + // and incoming_translations(id, translation, locale, revision)? Or just allow + // incoming_translations.message to be NULL? + $locale = $message_data->locale; + $entry = ORM::factory("incoming_translation") + ->where("key", "=", $key) + ->where("locale", "=", $locale) + ->find(); + if (!$entry->loaded()) { + // @todo Load a message key -> message (text) dict into memory outside of this loop + $root_entry = ORM::factory("incoming_translation") + ->where("key", "=", $key) + ->where("locale", "=", "root") + ->find(); + $entry->key = $key; + $entry->message = $root_entry->message; + $entry->locale = $locale; + } + $entry->revision = $revision; + $entry->translation = $translation; + $entry->save(); + } + + return $num_remaining; + } + + static function submit_translations() { + // Request format (HTTP POST): + // client_token = <client_token> + // uid = <l10n server user id> + // signature = md5(user_api_key($uid, $client_token) . $data . $client_token)) + // data = // JSON payload + // + // {<key_1>: {message: <JSON encoded message> + // translations: {<locale_1>: <JSON encoded translation>, + // <locale_2>: ...}}, + // <key_2>: {...} + // } + + // @todo Batch requests (max request size) + // @todo include base_revision in submission / how to handle resubmissions / edit fights? + $request = new stdClass(); + foreach (db::build() + ->select("key", "message", "locale", "base_revision", "translation") + ->from("outgoing_translations") + ->execute() as $row) { + $key = $row->key; + if (!isset($request->{$key})) { + $request->{$key} = new stdClass(); + $request->{$key}->translations = new stdClass(); + $request->{$key}->message = json_encode(unserialize($row->message)); + } + $request->{$key}->translations->{$row->locale} = json_encode(unserialize($row->translation)); + } + + // @todo reduce memory consumption, e.g. free $request + $request_data = json_encode($request); + $url = self::_server_url("submit"); + $signature = self::_sign($request_data); + + list ($response_data, $response_status) = remote::post( + $url, array("data" => $request_data, + "client_token" => l10n_client::client_token(), + "signature" => $signature, + "uid" => l10n_client::server_uid())); + + if (!remote::success($response_status)) { + throw new Exception("@todo TRANSLATIONS_SUBMISSION_FAILED " . $response_status); + } + if (empty($response_data)) { + return; + } + + $response = json_decode($response_data); + // Response format (JSON payload): + // [{key:<key_1>, locale:<locale_1>, rev:<rev_1>, status:<rejected|accepted|pending>}, + // {key:<key_2>, ...} + // ] + + // @todo Move messages out of outgoing into incoming, using new rev? + // @todo show which messages have been rejected / are pending? + } + + /** + * Plural forms. + */ + static function plural_forms($locale) { + $parts = explode('_', $locale); + $language = $parts[0]; + + // Data from CLDR 1.6 (http://unicode.org/cldr/data/common/supplemental/plurals.xml). + // Docs: http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html + switch ($language) { + case 'az': + case 'fa': + case 'hu': + case 'ja': + case 'ko': + case 'my': + case 'to': + case 'tr': + case 'vi': + case 'yo': + case 'zh': + case 'bo': + case 'dz': + case 'id': + case 'jv': + case 'ka': + case 'km': + case 'kn': + case 'ms': + case 'th': + return array('other'); + + case 'ar': + return array('zero', 'one', 'two', 'few', 'many', 'other'); + + case 'lv': + return array('zero', 'one', 'other'); + + case 'ga': + case 'se': + case 'sma': + case 'smi': + case 'smj': + case 'smn': + case 'sms': + return array('one', 'two', 'other'); + + case 'ro': + case 'mo': + case 'lt': + case 'cs': + case 'sk': + case 'pl': + return array('one', 'few', 'other'); + + case 'hr': + case 'ru': + case 'sr': + case 'uk': + case 'be': + case 'bs': + case 'sh': + case 'mt': + return array('one', 'few', 'many', 'other'); + + case 'sl': + return array('one', 'two', 'few', 'other'); + + case 'cy': + return array('one', 'two', 'many', 'other'); + + default: // en, de, etc. + return array('one', 'other'); + } + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/l10n_scanner.php b/modules/gallery/helpers/l10n_scanner.php new file mode 100644 index 0000000..5980ebe --- /dev/null +++ b/modules/gallery/helpers/l10n_scanner.php @@ -0,0 +1,178 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Scans all source code for messages that need to be localized. + */ +class l10n_scanner_Core { + // Based on Drupal's potx module, originally written by: + // Gbor Hojtsy http://drupal.org/user/4166 + public static $cache; + + static function process_message($message, &$cache) { + if (empty($cache)) { + foreach (db::build() + ->select("key") + ->from("incoming_translations") + ->where("locale", "=", "root") + ->execute() as $row) { + $cache[$row->key] = true; + } + } + + $key = Gallery_I18n::get_message_key($message); + if (array_key_exists($key, $cache)) { + return $cache[$key]; + } + + $entry = ORM::factory("incoming_translation")->where("key", "=", $key)->find(); + if (!$entry->loaded()) { + $entry->key = $key; + $entry->message = serialize($message); + $entry->locale = "root"; + $entry->save(); + } + } + + static function scan_php_file($file, &$cache) { + $code = file_get_contents($file); + $raw_tokens = token_get_all($code); + unset($code); + + $tokens = array(); + $func_token_list = array("t" => array(), "t2" => array()); + $token_number = 0; + // Filter out HTML / whitespace, and build a lookup for global function calls. + foreach ($raw_tokens as $token) { + if ((!is_array($token)) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) { + if (is_array($token)) { + if ($token[0] == T_STRING && in_array($token[1], array("t", "t2"))) { + $func_token_list[$token[1]][] = $token_number; + } + } + $tokens[] = $token; + $token_number++; + } + } + unset($raw_tokens); + + if (!empty($func_token_list["t"])) { + $errors = l10n_scanner::_parse_t_calls($tokens, $func_token_list["t"], $cache); + foreach ($errors as $line => $error) { + Kohana_Log::add( + "error", "Translation scanner error. " . + "file: " . substr($file, strlen(DOCROOT)) . ", line: $line, context: $error"); + } + } + + if (!empty($func_token_list["t2"])) { + $errors = l10n_scanner::_parse_plural_calls($tokens, $func_token_list["t2"], $cache); + foreach ($errors as $line => $error) { + Kohana_Log::add( + "error", "Translation scanner error. " . + "file: " . substr($file, strlen(DOCROOT)) . ", line: $line, context: $error"); + } + } + } + + static function scan_info_file($file, &$cache) { + $info = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS); + foreach (array('name', 'description') as $property) { + if (isset($info->$property)) { + l10n_scanner::process_message($info->$property, $cache); + } + } + } + + private static function _parse_t_calls(&$tokens, &$call_list, &$cache) { + $errors = array(); + foreach ($call_list as $index) { + $function_name = $tokens[$index++]; + $parens = $tokens[$index++]; + $first_param = $tokens[$index++]; + $next_token = $tokens[$index]; + + if ($parens == "(") { + if (in_array($next_token, array(")", ",")) + && (is_array($first_param) && ($first_param[0] == T_CONSTANT_ENCAPSED_STRING))) { + $message = self::_escape_quoted_string($first_param[1]); + l10n_scanner::process_message($message, $cache); + } else { + if (is_array($first_param) && ($first_param[0] == T_CONSTANT_ENCAPSED_STRING)) { + // Malformed string literals; escalate this + $errors[$first_param[2]] = + var_export(array($function_name, $parens, $first_param, $next_token), 1); + } else { + // t() found, but inside is something which is not a string literal. That's fine. + } + } + } + } + return $errors; + } + + private static function _parse_plural_calls(&$tokens, &$call_list, &$cache) { + $errors = array(); + foreach ($call_list as $index) { + $function_name = $tokens[$index++]; + $parens = $tokens[$index++]; + $first_param = $tokens[$index++]; + $first_separator = $tokens[$index++]; + $second_param = $tokens[$index++]; + $next_token = $tokens[$index]; + + if ($parens == "(") { + if ($first_separator == "," && $next_token == "," + && is_array($first_param) && $first_param[0] == T_CONSTANT_ENCAPSED_STRING + && is_array($second_param) && $second_param[0] == T_CONSTANT_ENCAPSED_STRING) { + $singular = self::_escape_quoted_string($first_param[1]); + $plural = self::_escape_quoted_string($second_param[1]); + l10n_scanner::process_message(array("one" => $singular, "other" => $plural), $cache); + } else { + if (is_array($first_param) && $first_param[0] == T_CONSTANT_ENCAPSED_STRING) { + $errors[$first_param[2]] = var_export( + array($function_name, $parens, $first_param, + $first_separator, $second_param, $next_token), 1); + } else { + // t2() found, but inside is something which is not a string literal. That's fine. + } + } + } + } + return $errors; + } + + /** + * Escape quotes in a strings depending on the surrounding + * quote type used. + * + * @param $str The strings to escape + */ + private static function _escape_quoted_string($str) { + $quo = substr($str, 0, 1); + $str = substr($str, 1, -1); + if ($quo == '"') { + $str = stripcslashes($str); + } else { + $str = strtr($str, array("\\'" => "'", "\\\\" => "\\")); + } + return $str; + } +} diff --git a/modules/gallery/helpers/legal_file.php b/modules/gallery/helpers/legal_file.php new file mode 100644 index 0000000..eb9c25d --- /dev/null +++ b/modules/gallery/helpers/legal_file.php @@ -0,0 +1,310 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class legal_file_Core { + private static $photo_types_by_extension; + private static $movie_types_by_extension; + private static $photo_extensions; + private static $movie_extensions; + private static $photo_types; + private static $movie_types; + private static $blacklist = array("php", "php3", "php4", "php5", "phtml", "phtm", "shtml", "shtm", + "pl", "cgi", "asp", "sh", "py", "c", "js"); + + /** + * Create a default list of allowed photo MIME types paired with their extensions and then let + * modules modify it. This is an ordered map, mapping extensions to their MIME types. + * Extensions cannot be duplicated, but MIMEs can (e.g. jpeg and jpg both map to image/jpeg). + * + * @param string $extension (opt.) - return MIME of extension; if not given, return complete array + */ + static function get_photo_types_by_extension($extension=null) { + if (empty(self::$photo_types_by_extension)) { + $types_by_extension_wrapper = new stdClass(); + $types_by_extension_wrapper->types_by_extension = array( + "jpg" => "image/jpeg", "jpeg" => "image/jpeg", "gif" => "image/gif", "png" => "image/png"); + module::event("photo_types_by_extension", $types_by_extension_wrapper); + foreach (self::$blacklist as $key) { + unset($types_by_extension_wrapper->types_by_extension[$key]); + } + self::$photo_types_by_extension = $types_by_extension_wrapper->types_by_extension; + } + if ($extension) { + // return matching MIME type + $extension = strtolower($extension); + if (isset(self::$photo_types_by_extension[$extension])) { + return self::$photo_types_by_extension[$extension]; + } else { + return null; + } + } else { + // return complete array + return self::$photo_types_by_extension; + } + } + + /** + * Create a default list of allowed movie MIME types paired with their extensions and then let + * modules modify it. This is an ordered map, mapping extensions to their MIME types. + * Extensions cannot be duplicated, but MIMEs can (e.g. jpeg and jpg both map to image/jpeg). + * + * @param string $extension (opt.) - return MIME of extension; if not given, return complete array + */ + static function get_movie_types_by_extension($extension=null) { + if (empty(self::$movie_types_by_extension)) { + $types_by_extension_wrapper = new stdClass(); + $types_by_extension_wrapper->types_by_extension = array( + "flv" => "video/x-flv", "mp4" => "video/mp4", "m4v" => "video/x-m4v"); + module::event("movie_types_by_extension", $types_by_extension_wrapper); + foreach (self::$blacklist as $key) { + unset($types_by_extension_wrapper->types_by_extension[$key]); + } + self::$movie_types_by_extension = $types_by_extension_wrapper->types_by_extension; + } + if ($extension) { + // return matching MIME type + $extension = strtolower($extension); + if (isset(self::$movie_types_by_extension[$extension])) { + return self::$movie_types_by_extension[$extension]; + } else { + return null; + } + } else { + // return complete array + return self::$movie_types_by_extension; + } + } + + /** + * Create a merged list of all allowed photo and movie MIME types paired with their extensions. + * + * @param string $extension (opt.) - return MIME of extension; if not given, return complete array + */ + static function get_types_by_extension($extension=null) { + $types_by_extension = legal_file::get_photo_types_by_extension(); + if (movie::allow_uploads()) { + $types_by_extension = array_merge($types_by_extension, + legal_file::get_movie_types_by_extension()); + } + if ($extension) { + // return matching MIME type + $extension = strtolower($extension); + if (isset($types_by_extension[$extension])) { + return $types_by_extension[$extension]; + } else { + return null; + } + } else { + // return complete array + return $types_by_extension; + } + } + + /** + * Create a default list of allowed photo extensions and then let modules modify it. + * + * @param string $extension (opt.) - return true if allowed; if not given, return complete array + */ + static function get_photo_extensions($extension=null) { + if (empty(self::$photo_extensions)) { + $extensions_wrapper = new stdClass(); + $extensions_wrapper->extensions = array_keys(legal_file::get_photo_types_by_extension()); + module::event("legal_photo_extensions", $extensions_wrapper); + self::$photo_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist); + } + if ($extension) { + // return true if in array, false if not + return in_array(strtolower($extension), self::$photo_extensions); + } else { + // return complete array + return self::$photo_extensions; + } + } + + /** + * Create a default list of allowed movie extensions and then let modules modify it. + * + * @param string $extension (opt.) - return true if allowed; if not given, return complete array + */ + static function get_movie_extensions($extension=null) { + if (empty(self::$movie_extensions)) { + $extensions_wrapper = new stdClass(); + $extensions_wrapper->extensions = array_keys(legal_file::get_movie_types_by_extension()); + module::event("legal_movie_extensions", $extensions_wrapper); + self::$movie_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist); + } + if ($extension) { + // return true if in array, false if not + return in_array(strtolower($extension), self::$movie_extensions); + } else { + // return complete array + return self::$movie_extensions; + } + } + + /** + * Create a merged list of all allowed photo and movie extensions. + * + * @param string $extension (opt.) - return true if allowed; if not given, return complete array + */ + static function get_extensions($extension=null) { + $extensions = legal_file::get_photo_extensions(); + if (movie::allow_uploads()) { + $extensions = array_merge($extensions, legal_file::get_movie_extensions()); + } + if ($extension) { + // return true if in array, false if not + return in_array(strtolower($extension), $extensions); + } else { + // return complete array + return $extensions; + } + } + + /** + * Create a merged list of all photo and movie filename filters, + * (e.g. "*.gif"), based on allowed extensions. + */ + static function get_filters() { + $filters = array(); + foreach (legal_file::get_extensions() as $extension) { + array_push($filters, "*." . $extension, "*." . strtoupper($extension)); + } + return $filters; + } + + /** + * Create a default list of allowed photo MIME types and then let modules modify it. + * Can be used to add legal alternatives for default MIME types. + * (e.g. flv maps to video/x-flv by default, but video/flv is still legal). + */ + static function get_photo_types() { + if (empty(self::$photo_types)) { + $types_wrapper = new stdClass(); + // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg). + $types_wrapper->types = array_unique(array_values(legal_file::get_photo_types_by_extension())); + module::event("legal_photo_types", $types_wrapper); + self::$photo_types = $types_wrapper->types; + } + return self::$photo_types; + } + + /** + * Create a default list of allowed movie MIME types and then let modules modify it. + * Can be used to add legal alternatives for default MIME types. + * (e.g. flv maps to video/x-flv by default, but video/flv is still legal). + */ + static function get_movie_types() { + if (empty(self::$movie_types)) { + $types_wrapper = new stdClass(); + // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg). + $types_wrapper->types = array_unique(array_values(legal_file::get_movie_types_by_extension())); + $types_wrapper->types[] = "video/flv"; + module::event("legal_movie_types", $types_wrapper); + self::$movie_types = $types_wrapper->types; + } + return self::$movie_types; + } + + /** + * Change the extension of a filename. If the original filename has no + * extension, add the new one to the end. + */ + static function change_extension($filename, $new_ext) { + $filename_no_ext = preg_replace("/\.[^\.\/]*?$/", "", $filename); + return "{$filename_no_ext}.{$new_ext}"; + } + + /** + * Reduce the given file to having a single extension. + */ + static function smash_extensions($filename) { + if (!$filename) { + // It's harmless, so return it before it causes issues with pathinfo. + return $filename; + } + $parts = pathinfo($filename); + $result = ""; + if ($parts["dirname"] != ".") { + $result .= $parts["dirname"] . "/"; + } + $parts["filename"] = str_replace(".", "_", $parts["filename"]); + $parts["filename"] = preg_replace("/[_]+/", "_", $parts["filename"]); + $parts["filename"] = trim($parts["filename"], "_"); + $result .= isset($parts["extension"]) ? "{$parts['filename']}.{$parts['extension']}" : $parts["filename"]; + return $result; + } + + /** + * Sanitize a filename for a given type (given as "photo" or "movie") and a target file format + * (given as an extension). This returns a completely legal and valid filename, + * or throws an exception if the type or extension given is invalid or illegal. It tries to + * maintain the filename's original extension even if it's not identical to the given extension + * (e.g. don't change "JPG" or "jpeg" to "jpg"). + * + * Note: it is not okay if the extension given is legal but does not match the type (e.g. if + * extension is "mp4" and type is "photo", it will throw an exception) + * + * @param string $filename (with no directory) + * @param string $extension (can be uppercase or lowercase) + * @param string $type (as "photo" or "movie") + * @return string sanitized filename (or null if bad extension argument) + */ + static function sanitize_filename($filename, $extension, $type) { + // Check if the type is valid - if so, get the mime types of the + // original and target extensions; if not, throw an exception. + $original_extension = pathinfo($filename, PATHINFO_EXTENSION); + switch ($type) { + case "photo": + $mime_type = legal_file::get_photo_types_by_extension($extension); + $original_mime_type = legal_file::get_photo_types_by_extension($original_extension); + break; + case "movie": + $mime_type = legal_file::get_movie_types_by_extension($extension); + $original_mime_type = legal_file::get_movie_types_by_extension($original_extension); + break; + default: + throw new Exception("@todo INVALID_TYPE"); + } + + // Check if the target extension is blank or invalid - if so, throw an exception. + if (!$extension || !$mime_type) { + throw new Exception("@todo ILLEGAL_EXTENSION"); + } + + // Check if the mime types of the original and target extensions match - if not, fix it. + if (!$original_extension || ($mime_type != $original_mime_type)) { + $filename = legal_file::change_extension($filename, $extension); + } + + // It should be a filename without a directory - remove all slashes (and backslashes). + $filename = str_replace("/", "_", $filename); + $filename = str_replace("\\", "_", $filename); + + // Remove extra dots from the filename. This will also remove extraneous underscores. + $filename = legal_file::smash_extensions($filename); + + // It's possible that the filename has no base (e.g. ".jpg") - if so, give it a generic one. + if (empty($filename) || (substr($filename, 0, 1) == ".")) { + $filename = $type . $filename; // e.g. "photo.jpg" or "movie.mp4" + } + + return $filename; + } +} diff --git a/modules/gallery/helpers/locales.php b/modules/gallery/helpers/locales.php new file mode 100644 index 0000000..8ca25c3 --- /dev/null +++ b/modules/gallery/helpers/locales.php @@ -0,0 +1,264 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling locales. + */ +class locales_Core { + private static $locales; + private static $language_subtag_to_locale; + + /** + * Return the list of available locales. + */ + static function available() { + if (empty(self::$locales)) { + self::_init_language_data(); + } + + return self::$locales; + } + + static function installed() { + $available = self::available(); + $default = module::get_var("gallery", "default_locale"); + $codes = explode("|", module::get_var("gallery", "installed_locales", $default)); + foreach ($codes as $code) { + if (isset($available[$code])) { + $installed[$code] = $available[$code]; + } + } + return $installed; + } + + static function update_installed($locales) { + // Ensure that the default is included... + $default = module::get_var("gallery", "default_locale"); + $locales = in_array($default, $locales) + ? $locales + : array_merge($locales, array($default)); + + module::set_var("gallery", "installed_locales", join("|", $locales)); + + // Clear the cache + self::$locales = null; + } + + // @todo Might want to add a localizable language name as well. + // ref: http://cldr.unicode.org/ + // ref: http://cldr.unicode.org/index/cldr-spec/picking-the-right-language-code + // ref: http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/likely_subtags.html + private static function _init_language_data() { + $l["af_ZA"] = "Afrikaans"; // Afrikaans + $l["ar_SA"] = "العربية"; // Arabic + $l["be_BY"] = "Беларускі"; // Belarusian + $l["bg_BG"] = "български"; // Bulgarian + $l["bn_BD"] = "বাংলা"; // Bengali + $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["fo_FO"] = "Føroyskt"; // Faroese + $l["fr_FR"] = "Français"; // French + $l["ga_IE"] = "Gaeilge"; // Irish + $l["he_IL"] = "עברית"; // Hebrew + $l["hr_HR"] = "hr̀vātskī"; // Croatian + $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["ms_MY"] = "Bahasa Melayu"; // Malay + $l["mk_MK"] = "Македонски јазик"; // Macedonian + $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["th_TH"] = "ภาษาไทย"; // Thai + $l["tn_ZA"] = "Setswana"; // Setswana + $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; + + // Language subtag to (default) locale mapping + foreach ($l as $locale => $name) { + list ($language) = explode("_", $locale . "_"); + // The first one mentioned is the default + if (!isset($d[$language])) { + $d[$language] = $locale; + } + } + self::$language_subtag_to_locale = $d; + } + + static function display_name($locale=null) { + if (empty(self::$locales)) { + self::_init_language_data(); + } + $locale or $locale = Gallery_I18n::instance()->locale(); + + return self::$locales[$locale]; + } + + static function is_rtl($locale=null) { + return Gallery_I18n::instance()->is_rtl($locale); + } + + /** + * Returns the best match comparing the HTTP accept-language header + * with the installed locales. + * @todo replace this with request::accepts_language() when we upgrade to Kohana 2.4 + */ + static function locale_from_http_request() { + $http_accept_language = Input::instance()->server("HTTP_ACCEPT_LANGUAGE"); + if ($http_accept_language) { + // Parse the HTTP header and build a preference list + // Example value: "de,en-us;q=0.7,en-uk,fr-fr;q=0.2" + $locale_preferences = array(); + foreach (explode(",", $http_accept_language) as $code) { + list ($requested_locale, $qvalue) = explode(";", $code . ";"); + $requested_locale = trim($requested_locale); + $qvalue = trim($qvalue); + if (preg_match("/^([a-z]{2,3})(?:[_-]([a-zA-Z]{2}))?/", $requested_locale, $matches)) { + $requested_locale = strtolower($matches[1]); + if (!empty($matches[2])) { + $requested_locale .= "_" . strtoupper($matches[2]); + } + $requested_locale = trim(str_replace("-", "_", $requested_locale)); + if (!strlen($qvalue)) { + // If not specified, default to 1. + $qvalue = 1; + } else { + // qvalue is expected to be something like "q=0.7" + list ($ignored, $qvalue) = explode("=", $qvalue . "=="); + $qvalue = floatval($qvalue); + } + // Group by language to boost inexact same-language matches + list ($language) = explode("_", $requested_locale . "_"); + if (!isset($locale_preferences[$language])) { + $locale_preferences[$language] = array(); + } + $locale_preferences[$language][$requested_locale] = $qvalue; + } + } + + // Compare and score requested locales with installed ones + $scored_locales = array(); + foreach ($locale_preferences as $language => $requested_locales) { + // Inexact match adjustment (same language, different region) + $fallback_adjustment_factor = 0.95; + if (count($requested_locales) > 1) { + // Sort by qvalue, descending + $qvalues = array_values($requested_locales); + rsort($qvalues); + // Ensure inexact match scores worse than 2nd preference in same language. + $fallback_adjustment_factor *= $qvalues[1]; + } + foreach ($requested_locales as $requested_locale => $qvalue) { + list ($matched_locale, $match_score) = + self::_locale_match_score($requested_locale, $qvalue, $fallback_adjustment_factor); + if ($matched_locale && + (!isset($scored_locales[$matched_locale]) || + $match_score > $scored_locales[$matched_locale])) { + $scored_locales[$matched_locale] = $match_score; + } + } + } + + arsort($scored_locales); + + list ($locale) = each($scored_locales); + return $locale; + } + + return null; + } + + private static function _locale_match_score($requested_locale, $qvalue, $adjustment_factor) { + $installed = locales::installed(); + if (isset($installed[$requested_locale])) { + return array($requested_locale, $qvalue); + } + list ($language) = explode("_", $requested_locale . "_"); + if (isset(self::$language_subtag_to_locale[$language]) && + isset($installed[self::$language_subtag_to_locale[$language]])) { + $score = $adjustment_factor * $qvalue; + return array(self::$language_subtag_to_locale[$language], $score); + } + return array(null, 0); + } + + static function set_request_locale() { + // 1. Check the session specific preference (cookie) + $locale = locales::cookie_locale(); + // 2. Check the user's preference + if (!$locale) { + $locale = identity::active_user()->locale; + } + // 3. Check the browser's / OS' preference + if (!$locale) { + $locale = locales::locale_from_http_request(); + } + // If we have any preference, override the site's default locale + if ($locale) { + Gallery_I18n::instance()->locale($locale); + } + } + + static function cookie_locale() { + // Can't use Input framework for client side cookies since + // they're not signed. + $cookie_data = isset($_COOKIE["g_locale"]) ? $_COOKIE["g_locale"] : null; + $locale = null; + if ($cookie_data) { + if (preg_match("/^([a-z]{2,3}(?:_[A-Z]{2})?)$/", trim($cookie_data), $matches)) { + $requested_locale = $matches[1]; + $installed_locales = locales::installed(); + if (isset($installed_locales[$requested_locale])) { + $locale = $requested_locale; + } + } + } + return $locale; + } +} diff --git a/modules/gallery/helpers/log.php b/modules/gallery/helpers/log.php new file mode 100644 index 0000000..cd554b5 --- /dev/null +++ b/modules/gallery/helpers/log.php @@ -0,0 +1,108 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class log_Core { + const SUCCESS = 1; + const INFO = 2; + const WARNING = 3; + const ERROR = 4; + + /** + * Report a successful event. + * @param string $category an arbitrary category we can use to filter log messages + * @param string $message a detailed log message + * @param string $html an html snippet presented alongside the log message to aid the admin + */ + static function success($category, $message, $html="") { + self::_add($category, $message, $html, log::SUCCESS); + } + + /** + * Report an informational event. + * @param string $category an arbitrary category we can use to filter log messages + * @param string $message a detailed log message + * @param string $html an html snippet presented alongside the log message to aid the admin + */ + static function info($category, $message, $html="") { + self::_add($category, $message, $html, log::INFO); + } + + /** + * Report that something went wrong, not fatal, but worth investigation. + * @param string $category an arbitrary category we can use to filter log messages + * @param string $message a detailed log message + * @param string $html an html snippet presented alongside the log message to aid the admin + */ + static function warning($category, $message, $html="") { + self::_add($category, $message, $html, log::WARNING); + } + + /** + * Report that something went wrong that should be fixed. + * @param string $category an arbitrary category we can use to filter log messages + * @param string $message a detailed log message + * @param string $html an html snippet presented alongside the log message to aid the admin + */ + static function error($category, $message, $html="") { + self::_add($category, $message, $html, log::ERROR); + } + + /** + * Add a log entry. + * + * @param string $category an arbitrary category we can use to filter log messages + * @param string $message a detailed log message + * @param integer $severity INFO, WARNING or ERROR + * @param string $html an html snippet presented alongside the log message to aid the admin + */ + private static function _add($category, $message, $html, $severity) { + $log = ORM::factory("log"); + $log->category = $category; + $log->message = $message; + $log->severity = $severity; + $log->html = $html; + $log->url = substr(url::abs_current(true), 0, 255); + $log->referer = request::referrer(null); + $log->timestamp = time(); + $log->user_id = identity::active_user()->id; + $log->save(); + } + + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case log::SUCCESS: + return "g-success"; + + case log::INFO: + return "g-info"; + + case log::WARNING: + return "g-warning"; + + case log::ERROR: + return "g-error"; + } + } +} diff --git a/modules/gallery/helpers/message.php b/modules/gallery/helpers/message.php new file mode 100644 index 0000000..d109995 --- /dev/null +++ b/modules/gallery/helpers/message.php @@ -0,0 +1,109 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class message_Core { + const SUCCESS = 1; + const INFO = 2; + const WARNING = 3; + const ERROR = 4; + + /** + * Report a successful event. + * @param string $msg a detailed message + */ + static function success($msg) { + self::_add($msg, message::SUCCESS); + } + + /** + * Report an informational event. + * @param string $msg a detailed message + */ + static function info($msg) { + self::_add($msg, message::INFO); + } + + /** + * Report that something went wrong, not fatal, but worth investigation. + * @param string $msg a detailed message + */ + static function warning($msg) { + self::_add($msg, message::WARNING); + } + + /** + * Report that something went wrong that should be fixed. + * @param string $msg a detailed message + */ + static function error($msg) { + self::_add($msg, message::ERROR); + } + + /** + * Save a message in the session for our next page load. + * @param string $msg a detailed message + * @param integer $severity one of the severity constants + */ + private static function _add($msg, $severity) { + $session = Session::instance(); + $status = $session->get("messages"); + $status[] = array($msg, $severity); + $session->set("messages", $status); + } + + /** + * Get any pending messages. There are two types of messages, transient and permanent. + * Permanent messages are used to let the admin know that there are pending administrative + * issues that need to be resolved. Transient ones are only displayed once. + * @return html text + */ + static function get() { + $buf = array(); + + $messages = Session::instance()->get_once("messages", array()); + foreach ($messages as $msg) { + $msg[0] = str_replace("__CSRF__", access::csrf_token(), $msg[0]); + $buf[] = "<li class=\"" . message::severity_class($msg[1]) . "\">$msg[0]</li>"; + } + if ($buf) { + return "<ul id=\"g-action-status\" class=\"g-message-block\">" . implode("", $buf) . "</ul>"; + } + } + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case message::SUCCESS: + return "g-success"; + + case message::INFO: + return "g-info"; + + case message::WARNING: + return "g-warning"; + + case message::ERROR: + return "g-error"; + } + } +} diff --git a/modules/gallery/helpers/model_cache.php b/modules/gallery/helpers/model_cache.php new file mode 100644 index 0000000..7cff68d --- /dev/null +++ b/modules/gallery/helpers/model_cache.php @@ -0,0 +1,42 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class model_cache_Core { + private static $cache = array(); + + static function get($model_name, $id, $field_name="id") { + if (TEST_MODE || empty(self::$cache[$model_name][$field_name][$id])) { + $model = ORM::factory($model_name)->where($field_name, "=", $id)->find(); + if (!$model->loaded()) { + throw new Exception("@todo MISSING_MODEL $model_name:$id"); + } + self::$cache[$model_name][$field_name][$id] = $model; + } + + return self::$cache[$model_name][$field_name][$id]; + } + + static function clear() { + self::$cache = array(); + } + + static function set($model) { + self::$cache[$model->object_name][$model->primary_key][$model->{$model->primary_key}] = $model; + } +} diff --git a/modules/gallery/helpers/module.php b/modules/gallery/helpers/module.php new file mode 100644 index 0000000..1b6c8d1 --- /dev/null +++ b/modules/gallery/helpers/module.php @@ -0,0 +1,594 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling modules. + * + * Note: by design, this class does not do any permission checking. + */ +class module_Core { + public static $active = array(); + public static $modules = array(); + public static $var_cache = null; + public static $available = array(); + + /** + * Set the version of the corresponding Module_Model + * @param string $module_name + * @param integer $version + */ + static function set_version($module_name, $version) { + $module = module::get($module_name); + if (!$module->loaded()) { + $module->name = $module_name; + $module->active = $module_name == "gallery"; // only gallery is active by default + } + $module->version = $version; + $module->save(); + Kohana_Log::add("debug", "$module_name: version is now $version"); + } + + /** + * Load the corresponding Module_Model + * @param string $module_name + */ + static function get($module_name) { + if (empty(self::$modules[$module_name])) { + return ORM::factory("module")->where("name", "=", $module_name)->find(); + } + return self::$modules[$module_name]; + } + + /** + * Get the information about a module + * @returns ArrayObject containing the module information from the module.info file or false if + * not found + */ + static function info($module_name) { + $module_list = module::available(); + return isset($module_list->$module_name) ? $module_list->$module_name : false; + } + + /** + * Check to see if a module is installed + * @param string $module_name + */ + static function is_installed($module_name) { + return array_key_exists($module_name, self::$modules); + } + + /** + * Check to see if a module is active + * @param string $module_name + */ + static function is_active($module_name) { + return array_key_exists($module_name, self::$modules) && + self::$modules[$module_name]->active; + } + + /** + * Return the list of available modules, including uninstalled modules. + */ + static function available() { + if (empty(self::$available)) { + $modules = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS); + foreach (glob(MODPATH . "*/module.info") as $file) { + $module_name = basename(dirname($file)); + $modules->$module_name = + new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS); + $m =& $modules->$module_name; + $m->installed = module::is_installed($module_name); + $m->active = module::is_active($module_name); + $m->code_version = $m->version; + $m->version = module::get_version($module_name); + $m->locked = false; + + if ($m->active && $m->version != $m->code_version) { + site_status::warning(t("Some of your modules are out of date. <a href=\"%upgrader_url\">Upgrade now!</a>", array("upgrader_url" => url::abs_site("upgrader"))), "upgrade_now"); + } + } + + // Lock certain modules + $modules->gallery->locked = true; + $identity_module = module::get_var("gallery", "identity_provider", "user"); + $modules->$identity_module->locked = true; + + $modules->uasort(array("module", "module_comparator")); + self::$available = $modules; + } + + return self::$available; + } + + /** + * Natural name sort comparator + */ + static function module_comparator($a, $b) { + return strnatcasecmp($a->name, $b->name); + } + + /** + * Return a list of all the active modules in no particular order. + */ + static function active() { + return self::$active; + } + + /** + * Check that the module can be activated. (i.e. all the prerequistes exist) + * @param string $module_name + * @return array an array of warning or error messages to be displayed + */ + static function can_activate($module_name) { + module::_add_to_path($module_name); + $messages = array(); + + $installer_class = "{$module_name}_installer"; + if (class_exists($installer_class) && method_exists($installer_class, "can_activate")) { + $messages = call_user_func(array($installer_class, "can_activate")); + } + + // Remove it from the active path + module::_remove_from_path($module_name); + return $messages; + } + + /** + * Allow modules to indicate the impact of deactivating the specified module + * @param string $module_name + * @return array an array of warning or error messages to be displayed + */ + static function can_deactivate($module_name) { + $data = (object)array("module" => $module_name, "messages" => array()); + + module::event("pre_deactivate", $data); + + return $data->messages; + } + + /** + * Install a module. This will call <module>_installer::install(), which is responsible for + * creating database tables, setting module variables and calling module::set_version(). + * Note that after installing, the module must be activated before it is available for use. + * @param string $module_name + */ + static function install($module_name) { + module::_add_to_path($module_name); + + $installer_class = "{$module_name}_installer"; + if (class_exists($installer_class) && method_exists($installer_class, "install")) { + call_user_func_array(array($installer_class, "install"), array()); + } + module::set_version($module_name, module::available()->$module_name->code_version); + + // Set the weight of the new module, which controls the order in which the modules are + // loaded. By default, new modules are installed at the end of the priority list. Since the + // id field is monotonically increasing, the easiest way to guarantee that is to set the weight + // the same as the id. We don't know that until we save it for the first time + $module = ORM::factory("module")->where("name", "=", $module_name)->find(); + if ($module->loaded()) { + $module->weight = $module->id; + $module->save(); + } + module::load_modules(); + + // Now the module is installed but inactive, so don't leave it in the active path + module::_remove_from_path($module_name); + + log::success( + "module", t("Installed module %module_name", array("module_name" => $module_name))); + } + + private static function _add_to_path($module_name) { + $config = Kohana_Config::instance(); + $kohana_modules = $config->get("core.modules"); + array_unshift($kohana_modules, MODPATH . $module_name); + $config->set("core.modules", $kohana_modules); + // Rebuild the include path so the module installer can benefit from auto loading + Kohana::include_paths(true); + } + + private static function _remove_from_path($module_name) { + $config = Kohana_Config::instance(); + $kohana_modules = $config->get("core.modules"); + if (($key = array_search(MODPATH . $module_name, $kohana_modules)) !== false) { + unset($kohana_modules[$key]); + $kohana_modules = array_values($kohana_modules); // reindex + } + $config->set("core.modules", $kohana_modules); + Kohana::include_paths(true); + } + + /** + * Upgrade a module. This will call <module>_installer::upgrade(), which is responsible for + * modifying database tables, changing module variables and calling module::set_version(). + * Note that after upgrading, the module must be activated before it is available for use. + * @param string $module_name + */ + static function upgrade($module_name) { + $version_before = module::get_version($module_name); + $installer_class = "{$module_name}_installer"; + $available = module::available(); + if (class_exists($installer_class) && method_exists($installer_class, "upgrade")) { + call_user_func_array(array($installer_class, "upgrade"), array($version_before)); + } else { + if (isset($available->$module_name->code_version)) { + module::set_version($module_name, $available->$module_name->code_version); + } else { + throw new Exception("@todo UNKNOWN_MODULE"); + } + } + module::load_modules(); + + $version_after = module::get_version($module_name); + if ($version_before != $version_after) { + log::success( + "module", t("Upgraded module %module_name from %version_before to %version_after", + array("module_name" => $module_name, + "version_before" => $version_before, + "version_after" => $version_after))); + } + + if ($version_after != $available->$module_name->code_version) { + throw new Exception("@todo MODULE_FAILED_TO_UPGRADE"); + } + } + + /** + * Activate an installed module. This will call <module>_installer::activate() which should take + * any steps to make sure that the module is ready for use. This will also activate any + * existing graphics rules for this module. + * @param string $module_name + */ + static function activate($module_name) { + module::_add_to_path($module_name); + + $installer_class = "{$module_name}_installer"; + if (class_exists($installer_class) && method_exists($installer_class, "activate")) { + call_user_func_array(array($installer_class, "activate"), array()); + } + + $module = module::get($module_name); + if ($module->loaded()) { + $module->active = true; + $module->save(); + } + module::load_modules(); + + graphics::activate_rules($module_name); + + block_manager::activate_blocks($module_name); + + log::success( + "module", t("Activated module %module_name", array("module_name" => $module_name))); + } + + /** + * Deactivate an installed module. This will call <module>_installer::deactivate() which should + * take any cleanup steps to make sure that the module isn't visible in any way. Note that the + * module remains available in Kohana's cascading file system until the end of the request! + * @param string $module_name + */ + static function deactivate($module_name) { + $installer_class = "{$module_name}_installer"; + if (class_exists($installer_class) && method_exists($installer_class, "deactivate")) { + call_user_func_array(array($installer_class, "deactivate"), array()); + } + + $module = module::get($module_name); + if ($module->loaded()) { + $module->active = false; + $module->save(); + } + module::load_modules(); + + graphics::deactivate_rules($module_name); + + block_manager::deactivate_blocks($module_name); + + if (module::info($module_name)) { + log::success( + "module", t("Deactivated module %module_name", array("module_name" => $module_name))); + } else { + log::success( + "module", t("Deactivated missing module %module_name", array("module_name" => $module_name))); + } + } + + /** + * Deactivate modules that are unavailable or missing, yet still active. + * This happens when a user deletes a module without deactivating it. + */ + static function deactivate_missing_modules() { + foreach (self::$modules as $module_name => $module) { + if (module::is_active($module_name) && !module::info($module_name)) { + module::deactivate($module_name); + } + } + } + + /** + * Uninstall a deactivated module. This will call <module>_installer::uninstall() which should + * take whatever steps necessary to make sure that all traces of a module are gone. + * @param string $module_name + */ + static function uninstall($module_name) { + $installer_class = "{$module_name}_installer"; + if (class_exists($installer_class) && method_exists($installer_class, "uninstall")) { + call_user_func(array($installer_class, "uninstall")); + } + + graphics::remove_rules($module_name); + $module = module::get($module_name); + if ($module->loaded()) { + $module->delete(); + } + module::load_modules(); + + // We could delete the module vars here too, but it's nice to leave them around + // in case the module gets reinstalled. + + log::success( + "module", t("Uninstalled module %module_name", array("module_name" => $module_name))); + } + + /** + * Load the active modules. This is called at bootstrap time. + */ + static function load_modules() { + self::$modules = array(); + self::$active = array(); + $kohana_modules = array(); + + // In version 32 we introduced a weight column so we can specify the module order + // If we try to use that blindly, we'll break earlier versions before they can even + // run the upgrader. + $modules = module::get_version("gallery") < 32 ? + ORM::factory("module")->find_all(): + ORM::factory("module")->order_by("weight")->find_all(); + + foreach ($modules as $module) { + self::$modules[$module->name] = $module; + if (!$module->active) { + continue; + } + + if ($module->name == "gallery") { + $gallery = $module; + } else { + self::$active[] = $module; + $kohana_modules[] = MODPATH . $module->name; + } + } + self::$active[] = $gallery; // put gallery last in the module list to match core.modules + $config = Kohana_Config::instance(); + $config->set( + "core.modules", array_merge($kohana_modules, $config->get("core.modules"))); + } + + /** + * Run a specific event on all active modules. + * @param string $name the event name + * @param mixed $data data to pass to each event handler + */ + static function event($name, &$data=null) { + $args = func_get_args(); + array_shift($args); + $function = str_replace(".", "_", $name); + + if (method_exists("gallery_event", $function)) { + switch (count($args)) { + case 0: + gallery_event::$function(); + break; + case 1: + gallery_event::$function($args[0]); + break; + case 2: + gallery_event::$function($args[0], $args[1]); + break; + case 3: + gallery_event::$function($args[0], $args[1], $args[2]); + break; + case 4: // Context menu events have 4 arguments so lets optimize them + gallery_event::$function($args[0], $args[1], $args[2], $args[3]); + break; + default: + call_user_func_array(array("gallery_event", $function), $args); + } + } + + foreach (self::$active as $module) { + if ($module->name == "gallery") { + continue; + } + $class = "{$module->name}_event"; + if (class_exists($class) && method_exists($class, $function)) { + call_user_func_array(array($class, $function), $args); + } + } + + // Give the admin theme a chance to respond, if we're in admin mode. + if (theme::$is_admin) { + $class = theme::$admin_theme_name . "_event"; + if (class_exists($class) && method_exists($class, $function)) { + call_user_func_array(array($class, $function), $args); + } + } + + // Give the site theme a chance to respond as well. It gets a chance even in admin mode, as + // long as the theme has an admin subdir. + $class = theme::$site_theme_name . "_event"; + if (class_exists($class) && method_exists($class, $function)) { + call_user_func_array(array($class, $function), $args); + } + } + + /** + * Get a variable from this module + * @param string $module_name + * @param string $name + * @param string $default_value + * @return the value + */ + static function get_var($module_name, $name, $default_value=null) { + // We cache vars so we can load them all at once for performance. + if (empty(self::$var_cache)) { + self::$var_cache = Cache::instance()->get("var_cache"); + if (empty(self::$var_cache)) { + // Cache doesn't exist, create it now. + foreach (db::build() + ->select("module_name", "name", "value") + ->from("vars") + ->order_by("module_name") + ->order_by("name") + ->execute() as $row) { + // Mute the "Creating default object from empty value" warning below + @self::$var_cache->{$row->module_name}->{$row->name} = $row->value; + } + Cache::instance()->set("var_cache", self::$var_cache, array("vars")); + } + } + + if (isset(self::$var_cache->$module_name->$name)) { + return self::$var_cache->$module_name->$name; + } else { + return $default_value; + } + } + + /** + * Store a variable for this module + * @param string $module_name + * @param string $name + * @param string $value + */ + static function set_var($module_name, $name, $value) { + $var = ORM::factory("var") + ->where("module_name", "=", $module_name) + ->where("name", "=", $name) + ->find(); + if (!$var->loaded()) { + $var->module_name = $module_name; + $var->name = $name; + } + $var->value = $value; + $var->save(); + + Cache::instance()->delete("var_cache"); + self::$var_cache = null; + } + + /** + * Increment the value of a variable for this module + * + * Note: Frequently updating counters is very inefficient because it invalidates the cache value + * which has to be rebuilt every time we make a change. + * + * @todo Get rid of this and find an alternate approach for all callers (currently only Akismet) + * + * @deprecated + * @param string $module_name + * @param string $name + * @param string $increment (optional, default is 1) + */ + static function incr_var($module_name, $name, $increment=1) { + db::build() + ->update("vars") + ->set("value", db::expr("`value` + $increment")) + ->where("module_name", "=", $module_name) + ->where("name", "=", $name) + ->execute(); + + Cache::instance()->delete("var_cache"); + self::$var_cache = null; + } + + /** + * Remove a variable for this module. + * @param string $module_name + * @param string $name + */ + static function clear_var($module_name, $name) { + db::build() + ->delete("vars") + ->where("module_name", "=", $module_name) + ->where("name", "=", $name) + ->execute(); + + Cache::instance()->delete("var_cache"); + self::$var_cache = null; + } + + /** + * Remove all variables for this module. + * @param string $module_name + */ + static function clear_all_vars($module_name) { + db::build() + ->delete("vars") + ->where("module_name", "=", $module_name) + ->execute(); + + Cache::instance()->delete("var_cache"); + self::$var_cache = null; + } + + /** + * Return the version of the installed module. + * @param string $module_name + */ + static function get_version($module_name) { + return module::get($module_name)->version; + } + + /** + * Check if obsolete modules are active and, if so, return a warning message. + * If none are found, return null. + */ + static function get_obsolete_modules_message() { + // This is the obsolete modules list. Any active module that's on the list + // with version number at or below the one given will be considered obsolete. + // It is hard-coded here, and may be updated with future releases of Gallery. + $obsolete_modules = array("videos" => 4, "noffmpeg" => 1, "videodimensions" => 1, + "digibug" => 2); + + // Before we check the active modules, deactivate any that are missing. + module::deactivate_missing_modules(); + + $modules_found = array(); + foreach ($obsolete_modules as $module => $version) { + if (module::is_active($module) && (module::get_version($module) <= $version)) { + $modules_found[] = $module; + } + } + + if ($modules_found) { + // Need this to be on one super-long line or else the localization scanner may not work. + // (ref: http://sourceforge.net/apps/trac/gallery/ticket/1321) + return t("Recent upgrades to Gallery have made the following modules obsolete: %modules. We recommend that you <a href=\"%url_mod\">deactivate</a> the module(s). For more information, please see the <a href=\"%url_doc\">documentation page</a>.", + array("modules" => implode(", ", $modules_found), + "url_mod" => url::site("admin/modules"), + "url_doc" => "http://codex.galleryproject.org/Gallery3:User_guide:Obsolete_modules")); + } + + return null; + } +} diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php new file mode 100644 index 0000000..2f19088 --- /dev/null +++ b/modules/gallery/helpers/movie.php @@ -0,0 +1,282 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling movies. + * + * Note: by design, this class does not do any permission checking. + */ +class movie_Core { + private static $allow_uploads; + + static function get_edit_form($movie) { + $form = new Forge("movies/update/$movie->id", "", "post", array("id" => "g-edit-movie-form")); + $form->hidden("from_id")->value($movie->id); + $group = $form->group("edit_item")->label(t("Edit Movie")); + $group->input("title")->label(t("Title"))->value($movie->title) + ->error_messages("required", t("You must provide a title")) + ->error_messages("length", t("Your title is too long")); + $group->textarea("description")->label(t("Description"))->value($movie->description); + $group->input("name")->label(t("Filename"))->value($movie->name) + ->error_messages( + "conflict", t("There is already a movie, photo or album with this name")) + ->error_messages("no_slashes", t("The movie name can't contain a \"/\"")) + ->error_messages("no_trailing_period", t("The movie name can't end in \".\"")) + ->error_messages("illegal_data_file_extension", t("You cannot change the movie file extension")) + ->error_messages("required", t("You must provide a movie file name")) + ->error_messages("length", t("Your movie file name is too long")); + $group->input("slug")->label(t("Internet Address"))->value($movie->slug) + ->error_messages( + "conflict", t("There is already a movie, photo or album with this internet address")) + ->error_messages( + "not_url_safe", + t("The internet address should contain only letters, numbers, hyphens and underscores")) + ->error_messages("required", t("You must provide an internet address")) + ->error_messages("length", t("Your internet address is too long")); + + module::event("item_edit_form", $movie, $form); + + $group = $form->group("buttons")->label(""); + $group->submit("")->value(t("Modify")); + + return $form; + } + + /** + * Extract a frame from a movie file. Valid movie_options are start_time (in seconds), + * input_args (extra ffmpeg input args) and output_args (extra ffmpeg output args). Extra args + * are added at the end of the list, so they can override any prior args. + * + * @param string $input_file + * @param string $output_file + * @param array $movie_options (optional) + */ + static function extract_frame($input_file, $output_file, $movie_options=null) { + $ffmpeg = movie::find_ffmpeg(); + if (empty($ffmpeg)) { + throw new Exception("@todo MISSING_FFMPEG"); + } + + list($width, $height, $mime_type, $extension, $duration) = movie::get_file_metadata($input_file); + + if (isset($movie_options["start_time"]) && is_numeric($movie_options["start_time"])) { + $start_time = max(0, $movie_options["start_time"]); // ensure it's non-negative + } else { + $start_time = module::get_var("gallery", "movie_extract_frame_time", 3); // use default + } + // extract frame at start_time, unless movie is too short + $start_time_arg = ($duration >= $start_time + 0.1) ? + "-ss " . movie::seconds_to_hhmmssdd($start_time) : ""; + + $input_args = isset($movie_options["input_args"]) ? $movie_options["input_args"] : ""; + $output_args = isset($movie_options["output_args"]) ? $movie_options["output_args"] : ""; + + $cmd = escapeshellcmd($ffmpeg) . " $input_args -i " . escapeshellarg($input_file) . + " -an $start_time_arg -an -r 1 -vframes 1" . + " -s {$width}x{$height}" . + " -y -f mjpeg $output_args " . escapeshellarg($output_file) . " 2>&1"; + exec($cmd, $exec_output, $exec_return); + + clearstatcache(); // use $filename parameter when PHP_version is 5.3+ + if (filesize($output_file) == 0 || $exec_return) { + // Maybe the movie needs the "-threads 1" argument added + // (see http://sourceforge.net/apps/trac/gallery/ticket/1924) + $cmd = escapeshellcmd($ffmpeg) . " -threads 1 $input_args -i " . escapeshellarg($input_file) . + " -an $start_time_arg -an -r 1 -vframes 1" . + " -s {$width}x{$height}" . + " -y -f mjpeg $output_args " . escapeshellarg($output_file) . " 2>&1"; + exec($cmd, $exec_output, $exec_return); + + clearstatcache(); + if (filesize($output_file) == 0 || $exec_return) { + throw new Exception("@todo FFMPEG_FAILED"); + } + } + } + + /** + * Return true if movie uploads are allowed, false if not. This is based on the + * "movie_allow_uploads" Gallery variable as well as whether or not ffmpeg is found. + */ + static function allow_uploads() { + if (empty(self::$allow_uploads)) { + // Refresh ffmpeg settings + $ffmpeg = movie::find_ffmpeg(); + switch (module::get_var("gallery", "movie_allow_uploads", "autodetect")) { + case "always": + self::$allow_uploads = true; + break; + case "never": + self::$allow_uploads = false; + break; + default: + self::$allow_uploads = !empty($ffmpeg); + break; + } + } + return self::$allow_uploads; + } + + /** + * Return the path to the ffmpeg binary if one exists and is executable, or null. + */ + static function find_ffmpeg() { + if (!($ffmpeg_path = module::get_var("gallery", "ffmpeg_path")) || + !@is_executable($ffmpeg_path)) { + $ffmpeg_path = system::find_binary( + "ffmpeg", module::get_var("gallery", "graphics_toolkit_path")); + module::set_var("gallery", "ffmpeg_path", $ffmpeg_path); + } + return $ffmpeg_path; + } + + /** + * Return version number and build date of ffmpeg if found, empty string(s) if not. When using + * static builds that aren't official releases, the version numbers are strange, hence why the + * date can be useful. + */ + static function get_ffmpeg_version() { + $ffmpeg = movie::find_ffmpeg(); + if (empty($ffmpeg)) { + return array("", ""); + } + + // Find version using -h argument since -version wasn't available in early versions. + // To keep the preg_match searches quick, we'll trim the (otherwise long) result. + $cmd = escapeshellcmd($ffmpeg) . " -h 2>&1"; + $result = substr(`$cmd`, 0, 1000); + if (preg_match("/ffmpeg version (\S+)/i", $result, $matches_version)) { + // Version number found - see if we can get the build date or copyright year as well. + if (preg_match("/built on (\S+\s\S+\s\S+)/i", $result, $matches_build_date)) { + return array(trim($matches_version[1], ","), trim($matches_build_date[1], ",")); + } else if (preg_match("/copyright \S*\s?2000-(\d{4})/i", $result, $matches_copyright_date)) { + return array(trim($matches_version[1], ","), $matches_copyright_date[1]); + } else { + return array(trim($matches_version[1], ","), ""); + } + } + return array("", ""); + } + + /** + * Return the width, height, mime_type, extension and duration of the given movie file. + * Metadata is first generated using ffmpeg (or set to defaults if it fails), + * then can be modified by other modules using movie_get_file_metadata events. + * + * This function and its use cases are symmetric to those of photo::get_file_metadata. + * + * @param string $file_path + * @return array array($width, $height, $mime_type, $extension, $duration) + * + * Use cases in detail: + * Input is standard movie type (flv/mp4/m4v) + * -> return metadata from ffmpeg + * Input is *not* standard movie type that is supported by ffmpeg (e.g. avi, mts...) + * -> return metadata from ffmpeg + * Input is *not* standard movie type that is *not* supported by ffmpeg but is legal + * -> return zero width, height, and duration; mime type and extension according to legal_file + * Input is illegal, unidentifiable, unreadable, or does not exist + * -> throw exception + * Note: movie_get_file_metadata events can change any of the above cases (except the last one). + */ + static function get_file_metadata($file_path) { + if (!is_readable($file_path)) { + throw new Exception("@todo UNREADABLE_FILE"); + } + + $metadata = new stdClass(); + $ffmpeg = movie::find_ffmpeg(); + if (!empty($ffmpeg)) { + // ffmpeg found - use it to get width, height, and duration. + $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($file_path) . " 2>&1"; + $result = `$cmd`; + if (preg_match("/Stream.*?Video:.*?, (\d+)x(\d+)/", $result, $matches_res)) { + if (preg_match("/Stream.*?Video:.*? \[.*?DAR (\d+):(\d+).*?\]/", $result, $matches_dar) && + $matches_dar[1] >= 1 && $matches_dar[2] >= 1) { + // DAR is defined - determine width based on height and DAR + // (should always be int, but adding round to be sure) + $matches_res[1] = round($matches_res[2] * $matches_dar[1] / $matches_dar[2]); + } + list ($metadata->width, $metadata->height) = array($matches_res[1], $matches_res[2]); + } else { + list ($metadata->width, $metadata->height) = array(0, 0); + } + + if (preg_match("/Duration: (\d+:\d+:\d+\.\d+)/", $result, $matches)) { + $metadata->duration = movie::hhmmssdd_to_seconds($matches[1]); + } else if (preg_match("/duration.*?:.*?(\d+)/", $result, $matches)) { + $metadata->duration = $matches[1]; + } else { + $metadata->duration = 0; + } + } else { + // ffmpeg not found - set width, height, and duration to zero. + $metadata->width = 0; + $metadata->height = 0; + $metadata->duration = 0; + } + + $extension = pathinfo($file_path, PATHINFO_EXTENSION); + if (!$extension || + (!$metadata->mime_type = legal_file::get_movie_types_by_extension($extension))) { + // Extension is empty or illegal. + $metadata->extension = null; + $metadata->mime_type = null; + } else { + // Extension is legal (and mime is already set above). + $metadata->extension = strtolower($extension); + } + + // Run movie_get_file_metadata events which can modify the class. + module::event("movie_get_file_metadata", $file_path, $metadata); + + // If the post-events results are invalid, throw an exception. Note that, unlike photos, having + // zero width and height isn't considered invalid (as is the case when FFmpeg isn't installed). + if (!$metadata->mime_type || !$metadata->extension || + ($metadata->mime_type != legal_file::get_movie_types_by_extension($metadata->extension))) { + throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE"); + } + + return array($metadata->width, $metadata->height, $metadata->mime_type, + $metadata->extension, $metadata->duration); + } + + /** + * Return the time/duration formatted in hh:mm:ss.dd from a number of seconds. + * Useful for inputs to ffmpeg. + * + * Note that this is similar to date("H:i:s", mktime(0,0,$seconds,0,0,0,0)), but unlike this + * approach avoids potential issues with time zone and DST mismatch and/or using deprecated + * features (the last argument of mkdate above, which disables DST, is deprecated as of PHP 5.3). + */ + static function seconds_to_hhmmssdd($seconds) { + return sprintf("%02d:%02d:%05.2f", floor($seconds / 3600), floor(($seconds % 3600) / 60), + floor(100 * $seconds % 6000) / 100); + } + + /** + * Return the number of seconds from a time/duration formatted in hh:mm:ss.dd. + * Useful for outputs from ffmpeg. + */ + static function hhmmssdd_to_seconds($hhmmssdd) { + preg_match("/(\d+):(\d+):(\d+\.\d+)/", $hhmmssdd, $matches); + return 3600 * $matches[1] + 60 * $matches[2] + $matches[3]; + } +} diff --git a/modules/gallery/helpers/photo.php b/modules/gallery/helpers/photo.php new file mode 100644 index 0000000..004cc7c --- /dev/null +++ b/modules/gallery/helpers/photo.php @@ -0,0 +1,145 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling photos. + * + * Note: by design, this class does not do any permission checking. + */ +class photo_Core { + static function get_edit_form($photo) { + $form = new Forge("photos/update/$photo->id", "", "post", array("id" => "g-edit-photo-form")); + $form->hidden("from_id")->value($photo->id); + $group = $form->group("edit_item")->label(t("Edit Photo")); + $group->input("title")->label(t("Title"))->value($photo->title) + ->error_messages("required", t("You must provide a title")) + ->error_messages("length", t("Your title is too long")); + $group->textarea("description")->label(t("Description"))->value($photo->description); + $group->input("name")->label(t("Filename"))->value($photo->name) + ->error_messages("conflict", t("There is already a movie, photo or album with this name")) + ->error_messages("no_slashes", t("The photo name can't contain a \"/\"")) + ->error_messages("no_trailing_period", t("The photo name can't end in \".\"")) + ->error_messages("illegal_data_file_extension", t("You cannot change the photo file extension")) + ->error_messages("required", t("You must provide a photo file name")) + ->error_messages("length", t("Your photo file name is too long")); + $group->input("slug")->label(t("Internet Address"))->value($photo->slug) + ->error_messages( + "conflict", t("There is already a movie, photo or album with this internet address")) + ->error_messages( + "not_url_safe", + t("The internet address should contain only letters, numbers, hyphens and underscores")) + ->error_messages("required", t("You must provide an internet address")) + ->error_messages("length", t("Your internet address is too long")); + + module::event("item_edit_form", $photo, $form); + + $group = $form->group("buttons")->label(""); + $group->submit("")->value(t("Modify")); + return $form; + } + + /** + * Return scaled width and height. + * + * @param integer $width + * @param integer $height + * @param integer $max the target size for the largest dimension + * @param string $format the output format using %d placeholders for width and height + */ + static function img_dimensions($width, $height, $max, $format="width=\"%d\" height=\"%d\"") { + if (!$width || !$height) { + return ""; + } + + if ($width > $height) { + $new_width = $max; + $new_height = (int)$max * ($height / $width); + } else { + $new_height = $max; + $new_width = (int)$max * ($width / $height); + } + return sprintf($format, $new_width, $new_height); + } + + /** + * Return the width, height, mime_type and extension of the given image file. + * Metadata is first generated using getimagesize (or the legal_file mapping if it fails), + * then can be modified by other modules using photo_get_file_metadata events. + * + * This function and its use cases are symmetric to those of photo::get_file_metadata. + * + * @param string $file_path + * @return array array($width, $height, $mime_type, $extension) + * + * Use cases in detail: + * Input is standard photo type (jpg/png/gif) + * -> return metadata from getimagesize() + * Input is *not* standard photo type that is supported by getimagesize (e.g. tif, bmp...) + * -> return metadata from getimagesize() + * Input is *not* standard photo type that is *not* supported by getimagesize but is legal + * -> return metadata if found by photo_get_file_metadata events + * Input is illegal, unidentifiable, unreadable, or does not exist + * -> throw exception + * Note: photo_get_file_metadata events can change any of the above cases (except the last one). + */ + static function get_file_metadata($file_path) { + if (!is_readable($file_path)) { + throw new Exception("@todo UNREADABLE_FILE"); + } + + $metadata = new stdClass(); + if ($image_info = getimagesize($file_path)) { + // getimagesize worked - use its results. + $metadata->width = $image_info[0]; + $metadata->height = $image_info[1]; + $metadata->mime_type = $image_info["mime"]; + $metadata->extension = image_type_to_extension($image_info[2], false); + // We prefer jpg instead of jpeg (which is returned by image_type_to_extension). + if ($metadata->extension == "jpeg") { + $metadata->extension = "jpg"; + } + } else { + // getimagesize failed - try to use legal_file mapping instead. + $extension = pathinfo($file_path, PATHINFO_EXTENSION); + if (!$extension || + (!$metadata->mime_type = legal_file::get_photo_types_by_extension($extension))) { + // Extension is empty or illegal. + $metadata->extension = null; + $metadata->mime_type = null; + } else { + // Extension is legal (and mime is already set above). + $metadata->extension = strtolower($extension); + } + $metadata->width = 0; + $metadata->height = 0; + } + + // Run photo_get_file_metadata events which can modify the class. + module::event("photo_get_file_metadata", $file_path, $metadata); + + // If the post-events results are invalid, throw an exception. + if (!$metadata->width || !$metadata->height || !$metadata->mime_type || !$metadata->extension || + ($metadata->mime_type != legal_file::get_photo_types_by_extension($metadata->extension))) { + throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE"); + } + + return array($metadata->width, $metadata->height, $metadata->mime_type, $metadata->extension); + } +} diff --git a/modules/gallery/helpers/random.php b/modules/gallery/helpers/random.php new file mode 100644 index 0000000..d40bfb5 --- /dev/null +++ b/modules/gallery/helpers/random.php @@ -0,0 +1,48 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class random_Core { + /** + * Return a random 32 byte hash value. + * @param string extra entropy data + */ + static function hash($length=32) { + require_once(MODPATH . "gallery/vendor/joomla/crypt.php"); + return md5(JCrypt::genRandomBytes($length)); + } + + /** + * Return a random floating point number between 0 and 1 + */ + static function percent() { + return ((float)mt_rand()) / (float)mt_getrandmax(); + } + + /** + * Return a random number between $min and $max. If $min and $max are not specified, + * return a random number between 0 and mt_getrandmax() + */ + static function int($min=null, $max=null) { + if ($min || $max) { + return mt_rand($min, $max); + } + return mt_rand(); + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/site_status.php b/modules/gallery/helpers/site_status.php new file mode 100644 index 0000000..e1a8163 --- /dev/null +++ b/modules/gallery/helpers/site_status.php @@ -0,0 +1,132 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class site_status_Core { + const SUCCESS = 1; + const INFO = 2; + const WARNING = 3; + const ERROR = 4; + + /** + * Report a successful event. + * @param string $msg a detailed message + * @param string $permanent_key make this message permanent and store it under this key + */ + static function success($msg, $permanent_key) { + self::_add($msg, self::SUCCESS, $permanent_key); + } + + /** + * Report an informational event. + * @param string $msg a detailed message + * @param string $permanent_key make this message permanent and store it under this key + */ + static function info($msg, $permanent_key) { + self::_add($msg, self::INFO, $permanent_key); + } + + /** + * Report that something went wrong, not fatal, but worth investigation. + * @param string $msg a detailed message + * @param string $permanent_key make this message permanent and store it under this key + */ + static function warning($msg, $permanent_key) { + self::_add($msg, self::WARNING, $permanent_key); + } + + /** + * Report that something went wrong that should be fixed. + * @param string $msg a detailed message + * @param string $permanent_key make this message permanent and store it under this key + */ + static function error($msg, $permanent_key) { + self::_add($msg, self::ERROR, $permanent_key); + } + + /** + * Save a message in the session for our next page load. + * @param string $msg a detailed message + * @param integer $severity one of the severity constants + * @param string $permanent_key make this message permanent and store it under this key + */ + private static function _add($msg, $severity, $permanent_key) { + $message = ORM::factory("message") + ->where("key", "=", $permanent_key) + ->find(); + if (!$message->loaded()) { + $message->key = $permanent_key; + } + $message->severity = $severity; + $message->value = $msg; + $message->save(); + } + + /** + * Remove any permanent message by key. + * @param string $permanent_key + */ + static function clear($permanent_key) { + $message = ORM::factory("message")->where("key", "=", $permanent_key)->find(); + if ($message->loaded()) { + $message->delete(); + } + } + + /** + * Get any pending messages. There are two types of messages, transient and permanent. + * Permanent messages are used to let the admin know that there are pending administrative + * issues that need to be resolved. Transient ones are only displayed once. + * @return html text + */ + static function get() { + if (!identity::active_user()->admin) { + return; + } + $buf = array(); + foreach (ORM::factory("message")->find_all() as $msg) { + $value = str_replace("__CSRF__", access::csrf_token(), $msg->value); + $buf[] = "<li class=\"" . site_status::severity_class($msg->severity) . "\">$value</li>"; + } + + if ($buf) { + return "<ul id=\"g-site-status\">" . implode("", $buf) . "</ul>"; + } + } + + /** + * Convert a message severity to a CSS class + * @param integer $severity + * @return string + */ + static function severity_class($severity) { + switch($severity) { + case site_status::SUCCESS: + return "g-success"; + + case site_status::INFO: + return "g-info"; + + case site_status::WARNING: + return "g-warning"; + + case site_status::ERROR: + return "g-error"; + } + } +} diff --git a/modules/gallery/helpers/system.php b/modules/gallery/helpers/system.php new file mode 100644 index 0000000..f0879d6 --- /dev/null +++ b/modules/gallery/helpers/system.php @@ -0,0 +1,113 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class system_Core { + private static $files_marked_for_deletion = array(); + + /** + * Return the path to an executable version of the named binary, or null. + * The paths are traversed in the following order: + * 1. $priority_path (if specified) + * 2. Gallery's own bin directory (DOCROOT . "bin") + * 3. PATH environment variable + * 4. extra_binary_paths Gallery variable (if specified) + * In addition, if the file is found inside Gallery's bin directory but + * it's not executable, we try to change its permissions to 0755. + * + * @param string $binary + * @param string $priority_path (optional) + * @return string path to binary if found; null if not found + */ + static function find_binary($binary, $priority_path=null) { + $bin_path = DOCROOT . "bin"; + + if ($priority_path) { + $paths = array($priority_path, $bin_path); + } else { + $paths = array($bin_path); + } + $paths = array_merge($paths, + explode(":", getenv("PATH")), + explode(":", module::get_var("gallery", "extra_binary_paths"))); + + foreach ($paths as $path) { + $path = rtrim($path, "/"); + $candidate = "$path/$binary"; + // @suppress errors below to avoid open_basedir issues + if (@file_exists($candidate)) { + if (!@is_executable($candidate) && + (substr_compare($bin_path, $candidate, 0, strlen($bin_path)) == 0)) { + // Binary isn't executable but is in Gallery's bin directory - try fixing permissions. + @chmod($candidate, 0755); + } + if (@is_executable($candidate)) { + return $candidate; + } + } + } + return null; + } + + /** + * Create a file with a unique file name. + * This helper is similar to the built-in tempnam. + * It allows the caller to specify a prefix and an extension. + * It always places the file in TMPPATH. + * Unless specified with the $delete_later argument, it will be marked + * for deletion at shutdown using system::delete_later. + */ + static function temp_filename($prefix="", $extension="", $delete_later=true) { + do { + $basename = tempnam(TMPPATH, $prefix); + if (!$basename) { + return false; + } + $filename = "$basename.$extension"; + $success = !file_exists($filename) && @rename($basename, $filename); + if (!$success) { + @unlink($basename); + } + } while (!$success); + + if ($delete_later) { + system::delete_later($filename); + } + + return $filename; + } + + /** + * Mark a file for deletion at shutdown time. This is useful for temp files, where we can delay + * the deletion time until shutdown to keep page load time quick. + */ + static function delete_later($filename) { + self::$files_marked_for_deletion[] = $filename; + } + + /** + * Delete all files marked using system::delete_later. This is called at gallery shutdown. + */ + static function delete_marked_files() { + foreach (self::$files_marked_for_deletion as $filename) { + // We want to suppress all errors, as it's possible that some of these + // files may have been deleted/moved before we got here. + @unlink($filename); + } + } +} diff --git a/modules/gallery/helpers/task.php b/modules/gallery/helpers/task.php new file mode 100644 index 0000000..5638faf --- /dev/null +++ b/modules/gallery/helpers/task.php @@ -0,0 +1,113 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class task_Core { + /** + * Get all available tasks + */ + static function get_definitions() { + $tasks = array(); + foreach (module::active() as $module) { + $class_name = "{$module->name}_task"; + if (class_exists($class_name) && method_exists($class_name, "available_tasks")) { + foreach (call_user_func(array($class_name, "available_tasks")) as $task) { + $tasks[$task->callback] = $task; + } + } + } + + return $tasks; + } + + static function start($task_callback, $context=array()) { + $tasks = task::get_definitions(); + $task = task::create($tasks[$task_callback], array()); + + $task->log(t("Task %task_name started (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id))); + return $task; + } + + static function create($task_def, $context) { + $task = ORM::factory("task"); + $task->callback = $task_def->callback; + $task->name = $task_def->name; + $task->percent_complete = 0; + $task->status = ""; + $task->state = "started"; + $task->owner_id = identity::active_user()->id; + $task->context = serialize($context); + $task->save(); + + return $task; + } + + static function cancel($task_id) { + $task = ORM::factory("task", $task_id); + if (!$task->loaded()) { + throw new Exception("@todo MISSING_TASK"); + } + $task->done = 1; + $task->state = "cancelled"; + $task->log(t("Task %task_name cancelled (task id %task_id)", + array("task_name" => $task->name, "task_id" => $task->id))); + $task->save(); + + return $task; + } + + static function remove($task_id) { + $task = ORM::factory("task", $task_id); + if ($task->loaded()) { + $task->delete(); + } + } + + static function run($task_id) { + $task = ORM::factory("task", $task_id); + if (!$task->loaded()) { + throw new Exception("@todo MISSING_TASK"); + } + + try { + $task->state = "running"; + call_user_func_array($task->callback, array(&$task)); + if ($task->done) { + $task->log($task->status); + } + $task->save(); + } catch (Exception $e) { + Kohana_Log::add("error", (string)$e); + + // Ugh. I hate to use instanceof, But this beats catching the exception separately since + // we mostly want to treat it the same way as all other exceptions + if ($e instanceof ORM_Validation_Exception) { + Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1)); + } + + $task->log((string)$e); + $task->state = "error"; + $task->done = true; + $task->status = substr($e->getMessage(), 0, 255); + $task->save(); + } + + return $task; + } +}
\ No newline at end of file diff --git a/modules/gallery/helpers/theme.php b/modules/gallery/helpers/theme.php new file mode 100644 index 0000000..072f98a --- /dev/null +++ b/modules/gallery/helpers/theme.php @@ -0,0 +1,113 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling themes. + * + * Note: by design, this class does not do any permission checking. + */ +class theme_Core { + public static $admin_theme_name; + public static $site_theme_name; + public static $is_admin; + + /** + * Load the active theme. This is called at bootstrap time. We will only ever have one theme + * active for any given request. + */ + static function load_themes() { + $input = Input::instance(); + $path = $input->server("PATH_INFO"); + if (empty($path)) { + $path = "/" . $input->get("kohana_uri"); + } + + $config = Kohana_Config::instance(); + $modules = $config->get("core.modules"); + + // Normally Router::find_uri() strips off the url suffix for us, but we're working off of the + // PATH_INFO here so we need to strip it off manually + if ($suffix = Kohana::config("core.url_suffix")) { + $path = preg_replace("#" . preg_quote($suffix) . "$#u", "", $path); + } + + self::$is_admin = $path == "/admin" || !strncmp($path, "/admin/", 7); + self::$site_theme_name = module::get_var("gallery", "active_site_theme"); + + // If the site theme doesn't exist, fall back to wind. + if (!file_exists(THEMEPATH . self::$site_theme_name . "/theme.info")) { + site_status::error(t("Theme '%name' is missing. Falling back to the Wind theme.", + array("name" => self::$site_theme_name)), "missing_site_theme"); + module::set_var("gallery", "active_site_theme", self::$site_theme_name = "wind"); + } + + if (self::$is_admin) { + // Load the admin theme + self::$admin_theme_name = module::get_var("gallery", "active_admin_theme"); + + // If the admin theme doesn't exist, fall back to admin_wind. + if (!file_exists(THEMEPATH . self::$admin_theme_name . "/theme.info")) { + site_status::error(t("Admin theme '%name' is missing! Falling back to the Wind theme.", + array("name" => self::$admin_theme_name)), "missing_admin_theme"); + module::set_var("gallery", "active_admin_theme", self::$admin_theme_name = "admin_wind"); + } + + array_unshift($modules, THEMEPATH . self::$admin_theme_name); + + // If the site theme has an admin subdir, load that as a module so that + // themes can provide their own code. + if (file_exists(THEMEPATH . self::$site_theme_name . "/admin")) { + array_unshift($modules, THEMEPATH . self::$site_theme_name . "/admin"); + } + // Admins can override the site theme, temporarily. This lets us preview themes. + if (identity::active_user()->admin && $override = $input->get("theme")) { + if (file_exists(THEMEPATH . $override)) { + self::$admin_theme_name = $override; + array_unshift($modules, THEMEPATH . self::$admin_theme_name); + } else { + Kohana_Log::add("error", "Missing override admin theme: '$override'"); + } + } + } else { + // Admins can override the site theme, temporarily. This lets us preview themes. + if (identity::active_user()->admin && $override = $input->get("theme")) { + if (file_exists(THEMEPATH . $override)) { + self::$site_theme_name = $override; + } else { + Kohana_Log::add("error", "Missing override site theme: '$override'"); + } + } + array_unshift($modules, THEMEPATH . self::$site_theme_name); + } + + $config->set("core.modules", $modules); + } + + static function get_info($theme_name) { + $theme_name = preg_replace("/[^a-zA-Z0-9\._-]/", "", $theme_name); + $file = THEMEPATH . "$theme_name/theme.info"; + $theme_info = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS); + $theme_info->description = t($theme_info->description); + $theme_info->name = t($theme_info->name); + + return $theme_info; + } +} + diff --git a/modules/gallery/helpers/tree_rest.php b/modules/gallery/helpers/tree_rest.php new file mode 100644 index 0000000..5186cf0 --- /dev/null +++ b/modules/gallery/helpers/tree_rest.php @@ -0,0 +1,92 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tree_rest_Core { + /** + * The tree is rooted in a single item and can have modifiers which adjust what data is shown + * for items inside the given tree, up to the depth that you want. The entity for this resource + * is a series of items. + * + * depth=<number> + * Only traverse this far down into the tree. If there are more albums + * below this depth, provide RESTful urls to other tree resources in + * the members section. Default is infinite. + * + * type=<album|photo|movie> + * Restrict the items displayed to the given type. Default is all types. + * + * fields=<comma separated list of field names> + * In the entity section only return these fields for each item. + * Default is all fields. + */ + static function get($request) { + $item = rest::resolve($request->url); + access::required("view", $item); + + $query_params = array(); + $p = $request->params; + $where = array(); + if (isset($p->type)) { + $where[] = array("type", "=", $p->type); + $query_params[] = "type={$p->type}"; + } + + if (isset($p->depth)) { + $lowest_depth = $item->level + $p->depth; + $where[] = array("level", "<=", $lowest_depth); + $query_params[] = "depth={$p->depth}"; + } + + $fields = array(); + if (isset($p->fields)) { + $fields = explode(",", $p->fields); + $query_params[] = "fields={$p->fields}"; + } + + $entity = array(array("url" => rest::url("item", $item), + "entity" => $item->as_restful_array($fields))); + $members = array(); + foreach ($item->viewable()->descendants(null, null, $where) as $child) { + $entity[] = array("url" => rest::url("item", $child), + "entity" => $child->as_restful_array($fields)); + if (isset($lowest_depth) && $child->level == $lowest_depth) { + $members[] = url::merge_querystring(rest::url("tree", $child), $query_params); + } + } + + $result = array( + "url" => $request->url, + "entity" => $entity, + "members" => $members, + "relationships" => rest::relationships("tree", $item)); + return $result; + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + return $item; + } + + static function url($item) { + return url::abs_site("rest/tree/{$item->id}"); + } +} diff --git a/modules/gallery/helpers/upgrade_checker.php b/modules/gallery/helpers/upgrade_checker.php new file mode 100644 index 0000000..492f72e --- /dev/null +++ b/modules/gallery/helpers/upgrade_checker.php @@ -0,0 +1,105 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class upgrade_checker_Core { + const CHECK_URL = "http://galleryproject.org/versioncheck/gallery3"; + const AUTO_CHECK_INTERVAL = 604800; // 7 days in seconds + + /** + * Return the last version info blob retrieved from the Gallery website or + * null if no checks have been performed. + */ + static function version_info() { + return unserialize(Cache::instance()->get("upgrade_checker_version_info")); + } + + /** + * Return true if auto checking is enabled. + */ + static function auto_check_enabled() { + return (bool)module::get_var("gallery", "upgrade_checker_auto_enabled"); + } + + /** + * Return true if it's time to auto check. + */ + static function should_auto_check() { + if (upgrade_checker::auto_check_enabled() && random::int(1, 100) == 1) { + $version_info = upgrade_checker::version_info(); + return (!$version_info || + (time() - $version_info->timestamp) > upgrade_checker::AUTO_CHECK_INTERVAL); + } + return false; + } + + /** + * Fech version info from the Gallery website. + */ + static function fetch_version_info() { + $result = new stdClass(); + try { + list ($status, $headers, $body) = remote::do_request(upgrade_checker::CHECK_URL); + if ($status == "HTTP/1.1 200 OK") { + $result->status = "success"; + foreach (explode("\n", $body) as $line) { + if ($line) { + list($key, $val) = explode("=", $line, 2); + $result->data[$key] = $val; + } + } + } else { + $result->status = "error"; + } + } catch (Exception $e) { + Kohana_Log::add("error", + sprintf("%s in %s at line %s:\n%s", $e->getMessage(), $e->getFile(), + $e->getLine(), $e->getTraceAsString())); + } + $result->timestamp = time(); + Cache::instance()->set("upgrade_checker_version_info", serialize($result), + array("upgrade"), 86400 * 365); + } + + /** + * Check the latest version info blob to see if it's time for an upgrade. + */ + static function get_upgrade_message() { + $version_info = upgrade_checker::version_info(); + if ($version_info) { + if (gallery::RELEASE_CHANNEL == "release") { + if (version_compare($version_info->data["release_version"], gallery::VERSION, ">")) { + return t("A newer version of Gallery is available! <a href=\"%upgrade-url\">Upgrade now</a> to version %version", + array("version" => $version_info->data["release_version"], + "upgrade-url" => $version_info->data["release_upgrade_url"])); + } + } else { + $branch = gallery::RELEASE_BRANCH; + if (isset($version_info->data["branch_{$branch}_build_number"]) && + version_compare($version_info->data["branch_{$branch}_build_number"], + gallery::build_number(), ">")) { + return t("A newer version of Gallery is available! <a href=\"%upgrade-url\">Upgrade now</a> to version %version (build %build on branch %branch)", + array("version" => $version_info->data["branch_{$branch}_version"], + "upgrade-url" => $version_info->data["branch_{$branch}_upgrade_url"], + "build" => $version_info->data["branch_{$branch}_build_number"], + "branch" => $branch)); + } + } + } + } +} diff --git a/modules/gallery/helpers/user_profile.php b/modules/gallery/helpers/user_profile.php new file mode 100644 index 0000000..222d2f5 --- /dev/null +++ b/modules/gallery/helpers/user_profile.php @@ -0,0 +1,55 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class user_profile_Core { + /** + * Generate the url to display the profile + * @return url for the profile display + */ + static function url($user_id) { + return url::site("user_profile/show/{$user_id}"); + } + + static function get_contact_form($user) { + $form = new Forge("user_profile/send/{$user->id}", "", "post", + array("id" => "g-user-profile-contact-form")); + $group = $form->group("message") + ->label(t("Compose message to %name", array("name" => $user->display_name()))); + $group->input("reply_to") + ->label(t("From:")) + ->rules("required|length[1, 256]|valid_email") + ->error_messages("required", t("You must enter a valid email address")) + ->error_messages("max_length", t("Your email address is too long")) + ->error_messages("valid_email", t("You must enter a valid email address")); + $group->input("subject") + ->label(t("Subject:")) + ->rules("required|length[1, 256]") + ->error_messages("required", t("Your message must have a subject")) + ->error_messages("max_length", t("Your subject is too long")); + $group->textarea("message") + ->label(t("Message:")) + ->rules("required") + ->error_messages("required", t("You must enter a message")); + module::event("user_profile_contact_form", $form); + module::event("captcha_protect_form", $form); + $group->submit("")->value(t("Send")); + return $form; + } +} diff --git a/modules/gallery/helpers/xml.php b/modules/gallery/helpers/xml.php new file mode 100644 index 0000000..e20beb1 --- /dev/null +++ b/modules/gallery/helpers/xml.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class xml_Core { + static function to_xml($array, $element_names) { + $xml = "<$element_names[0]>\n"; + foreach ($array as $key => $value) { + if (is_array($value)) { + $xml .= xml::to_xml($value, array_slice($element_names, 1)); + } else if (is_object($value)) { + $xml .= xml::to_xml($value->as_array(), array_slice($element_names, 1)); + } else { + $xml .= "<$key>$value</$key>\n"; + } + } + $xml .= "</$element_names[0]>\n"; + return $xml; + } +} diff --git a/modules/gallery/hooks/init_gallery.php b/modules/gallery/hooks/init_gallery.php new file mode 100644 index 0000000..fa4f816 --- /dev/null +++ b/modules/gallery/hooks/init_gallery.php @@ -0,0 +1,56 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// If var/database.php doesn't exist, then we assume that the Gallery is not properly installed +// and send users to the installer. +if (!file_exists(VARPATH . "database.php")) { + url::redirect(url::abs_file("installer")); +} + +// Simple and cheap test to make sure that the database config is ok. Do this before we do +// anything else database related. +try { + Database::instance()->connect(); +} catch (Kohana_PHP_Exception $e) { + print "Database configuration error. Please check var/database.php"; + exit; +} + +Event::add("system.ready", array("Gallery_I18n", "instance")); +Event::add("system.ready", array("module", "load_modules")); +Event::add("system.ready", array("gallery", "ready")); +Event::add("system.post_routing", array("url", "parse_url")); +Event::add("system.post_routing", array("gallery", "maintenance_mode")); +Event::add("system.post_routing", array("gallery", "private_gallery")); +Event::add("system.shutdown", array("gallery", "shutdown")); + +// @todo once we convert to Kohana 2.4 this doesn't have to be here +set_error_handler(array("gallery_error", "error_handler")); + +// Override the cookie if we have a session id in the URL. +// @todo This should probably be an event callback +$input = Input::instance(); +if ($g3sid = $input->post("g3sid", $input->get("g3sid"))) { + $_COOKIE["g3sid"] = $g3sid; +} + +if ($user_agent = $input->post("user_agent", $input->get("user_agent"))) { + $_SERVER["HTTP_USER_AGENT"] = $user_agent; +} diff --git a/modules/gallery/images/ffmpeg.png b/modules/gallery/images/ffmpeg.png Binary files differnew file mode 100644 index 0000000..6be8b62 --- /dev/null +++ b/modules/gallery/images/ffmpeg.png diff --git a/modules/gallery/images/gallery.png b/modules/gallery/images/gallery.png Binary files differnew file mode 100644 index 0000000..ca8e0e9 --- /dev/null +++ b/modules/gallery/images/gallery.png diff --git a/modules/gallery/images/gd.png b/modules/gallery/images/gd.png Binary files differnew file mode 100644 index 0000000..b341d71 --- /dev/null +++ b/modules/gallery/images/gd.png diff --git a/modules/gallery/images/graphicsmagick.png b/modules/gallery/images/graphicsmagick.png Binary files differnew file mode 100644 index 0000000..3d1d77e --- /dev/null +++ b/modules/gallery/images/graphicsmagick.png diff --git a/modules/gallery/images/imagemagick.jpg b/modules/gallery/images/imagemagick.jpg Binary files differnew file mode 100644 index 0000000..d83c450 --- /dev/null +++ b/modules/gallery/images/imagemagick.jpg diff --git a/modules/gallery/images/missing_album_cover.jpg b/modules/gallery/images/missing_album_cover.jpg Binary files differnew file mode 100644 index 0000000..bdddeec --- /dev/null +++ b/modules/gallery/images/missing_album_cover.jpg diff --git a/modules/gallery/images/missing_movie.jpg b/modules/gallery/images/missing_movie.jpg Binary files differnew file mode 100644 index 0000000..452db22 --- /dev/null +++ b/modules/gallery/images/missing_movie.jpg diff --git a/modules/gallery/images/missing_photo.jpg b/modules/gallery/images/missing_photo.jpg Binary files differnew file mode 100644 index 0000000..a9d176d --- /dev/null +++ b/modules/gallery/images/missing_photo.jpg diff --git a/modules/gallery/js/albums_form_add.js b/modules/gallery/js/albums_form_add.js new file mode 100644 index 0000000..4e22760 --- /dev/null +++ b/modules/gallery/js/albums_form_add.js @@ -0,0 +1,25 @@ +$("#g-add-album-form input[name=title]").change( + function() { + console.log("changing title"); + $("#g-add-album-form input[name=name]").attr( + "value", $("#g-add-album-form input[name=title]").attr("value") + .replace(/[\s\/]+/g, "-").replace(/\.+$/, "")); + $("#g-add-album-form input[name=slug]").attr( + "value", $("#g-add-album-form input[name=title]").attr("value") + .replace(/[^A-Za-z0-9-_]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "")); + }); +$("#g-add-album-form input[name=title]").keyup( + function() { + console.log("key up"); + $("#g-add-album-form input[name=name]").attr( + "value", $("#g-add-album-form input[name=title]").attr("value") + .replace(/[\s\/]+/g, "-") + .replace(/\.+$/, "")); + $("#g-add-album-form input[name=slug]").attr( + "value", $("#g-add-album-form input[name=title]").attr("value") + .replace(/[^A-Za-z0-9-_]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "")); + }); diff --git a/modules/gallery/js/item_form_delete.js b/modules/gallery/js/item_form_delete.js new file mode 100644 index 0000000..fa3f24a --- /dev/null +++ b/modules/gallery/js/item_form_delete.js @@ -0,0 +1,5 @@ +$("#g-confirm-delete").submit( + function() { + $("#g-confirm-delete input[type=submit]").gallery_show_loading(); + } +); diff --git a/modules/gallery/js/l10n_client.js b/modules/gallery/js/l10n_client.js new file mode 100644 index 0000000..6d919c2 --- /dev/null +++ b/modules/gallery/js/l10n_client.js @@ -0,0 +1,315 @@ +// Fork from Drupal's l10n_client module, originally written by: +// Gbor Hojtsy http://drupal.org/user/4166 (original author) +// Young Hahn / Development Seed - http://developmentseed.org/ (friendly user interface) + +var Gallery = Gallery || { 'behaviors': {} }; + +Gallery.attachBehaviors = function(context) { + context = context || document; + // Execute all of them. + jQuery.each(Gallery.behaviors, + function() { + this(context); + }); +}; + +$(document).ready(function() { + Gallery.attachBehaviors(this); +}); + +// Store all l10n_client related data + methods in its own object +jQuery.extend(Gallery, { + l10nClient: new (function() { + // Set "selected" string to unselected, i.e. -1 + this.selected = -1; + // Keybindings + this.keys = {'toggle':'ctrl+shift+s', 'clear': 'esc'}; // Keybindings + // Keybinding functions + this.key = function(pressed) { + switch(pressed) { + case 'toggle': + // Grab user-hilighted text & send it into the search filter + userSelection = window.getSelection ? window.getSelection() : document.getSelection ? document.getSelection() : document.selection.createRange().text; + userSelection = String(userSelection); + if(userSelection.length > 0) { + Gallery.l10nClient.filter(userSelection); + Gallery.l10nClient.toggle(1); + $('#l10n-client #g-l10n-search').focus(); + } else { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + if(!$.browser.safari) { + $('#l10n-client #g-l10n-search').focus(); + } + } else { + Gallery.l10nClient.toggle(0); + } + } + break; + case 'clear': + this.filter(false); + break; + } + }; + + // Toggle the l10nclient + this.toggle = function(state) { + switch(state) { + case 1: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').show(); + $('#l10n-client').height('22em').removeClass('hidden'); + //$('#l10n-client').slideUp(); + $('#g-minimize-l10n').text("_"); + // This CSS clashes with Gallery's CSS, probably due to + // YUI's grid / floats. + // if(!$.browser.msie) { + // $('body').css('border-bottom', '22em solid #fff'); + // } + $.cookie('Gallery_l10n_client', '1', {expires: 7, path: '/'}); + break; + case 0: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').hide(); + $('#l10n-client').height('2em').addClass('hidden'); + // TODO: Localize this message + $('#g-minimize-l10n').text(MSG_TRANSLATE_TEXT); + // if(!$.browser.msie) { + // $('body').css('border-bottom', '0px'); + // } + $.cookie('Gallery_l10n_client', '0', {expires: 7, path: '/'}); + break; + } + }; + + // Get a string from the DOM tree + this.getString = function(index, type) { + if (index < l10n_client_data.length) { + return l10n_client_data[index][type]; + } + return ""; + }; + + // Set a string in the DOM tree + this.setString = function(index, data) { + l10n_client_data[index]['translation'] = data; + }; + + // Display the source message + this.showSourceMessage = function(source, is_plural) { + if (is_plural) { + var pretty_source = $('#source-text-tmp-space').text('[one] - ' + source['one']).html(); + pretty_source += '<br/>'; + pretty_source += $('#source-text-tmp-space').text('[other] - ' + source['other']).html(); + } else { + var pretty_source = $('#source-text-tmp-space').text(source).html(); + } + $('#l10n-client-string-editor .source-text').html(pretty_source); + }; + this.isPluralMessage = function(message) { + return typeof(message) == 'object'; + }; + + this.updateTranslationForm = function(translation, is_plural) { + $('.translationField').addClass('hidden'); + if (is_plural) { + if (typeof(translation) != 'object') { + translation = {}; + } + var num_plural_forms = plural_forms.length; + for (var i = 0; i < num_plural_forms; i++) { + var form = plural_forms[i]; + if (translation[form] == undefined) { + translation[form] = ''; + } + $("#plural-" + form + " textarea[name='l10n-edit-plural-translation-" + form + "']") + .attr('value', translation[form]); + $('#plural-' + form).removeClass('hidden'); + } + } else { + $('#l10n-edit-translation').attr('value', translation); + $('#l10n-edit-translation').removeClass('hidden'); + } + }; + + // Filter the string list by a search string + this.filter = function(search) { + if(search == false || search == '') { + $('#l10n-client #l10n-search-filter-clear').focus(); + $('#l10n-client-string-select li').show(); + $('#l10n-client #g-l10n-search').val(''); + $('#l10n-client #g-l10n-search').focus(); + } else { + if(search.length > 0) { + $('#l10n-client-string-select li').hide(); + $('#l10n-client-string-select li').each(function() { + if ($(this).val().indexOf(search) != -1) { + $(this).show(); + } + }); + $('#l10n-client #g-l10n-search').val(search); + } + } + }; + + this.copySourceText = function() { + var index = Gallery.l10nClient.selected; + if (index >= 0) { + var source = Gallery.l10nClient.getString(index, 'source'); + var is_plural = Gallery.l10nClient.isPluralMessage(source); + if (is_plural) { + if (typeof(translation) != 'object') { + translation = {}; + } + var num_plural_forms = plural_forms.length; + for (var i = 0; i < num_plural_forms; i++) { + var form = plural_forms[i]; + var text = source['other']; + if (form == 'one') { + text = source['one']; + } + $("#plural-" + form + " textarea[name='l10n-edit-plural-translation-" + form + "']") + .attr('value', text); + } + } else { + $('#l10n-edit-translation').attr('value', source); + } + + } + }; + }) +}); + +// Attaches the localization editor behavior to all required fields. +Gallery.behaviors.l10nClient = function(context) { + + switch($.cookie('Gallery_l10n_client')) { + case '1': + Gallery.l10nClient.toggle(1); + break; + default: + Gallery.l10nClient.toggle(0); + break; + } + + // If the selection changes, copy string values to the source and target fields. + // Add class to indicate selected string in list widget. + $('#l10n-client-string-select li').click(function() { + $('#l10n-client-string-select li').removeClass('active'); + $(this).addClass('active'); + var index = $('#l10n-client-string-select li').index(this); + var source = Gallery.l10nClient.getString(index, 'source'); + var key = Gallery.l10nClient.getString(index, 'key'); + var is_plural = Gallery.l10nClient.isPluralMessage(source); + Gallery.l10nClient.showSourceMessage(source, is_plural); + Gallery.l10nClient.updateTranslationForm(Gallery.l10nClient.getString(index, 'translation'), is_plural); + $("#g-l10n-client-save-form input[name='l10n-message-key']").val(key); + Gallery.l10nClient.selected = index; + }); + + // When l10n_client window is clicked, toggle based on current state. + $('#g-minimize-l10n').click(function() { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + } else { + Gallery.l10nClient.toggle(0); + } + }); + + // Close the l10n client using an AJAX call and refreshing the page + $('#g-close-l10n').click(function(event) { + $.ajax({ + type: "GET", + url: toggle_l10n_mode_url, + data: "csrf=" + csrf, + success: function() { + window.location.reload(true); + } + }); + event.preventDefault(); + }); + + // Register keybindings using jQuery hotkeys + // TODO: Either remove hotkeys code or add query.hotkeys.js. + if($.hotkeys) { + $.hotkeys.add(Gallery.l10nClient.keys['toggle'], function(){Gallery.l10nClient.key('toggle');}); + $.hotkeys.add(Gallery.l10nClient.keys['clear'], {target:'#l10n-client #g-l10n-search', type:'keyup'}, function(){Gallery.l10nClient.key('clear');}); + } + + // never actually submit the form as the search is done in the browser + $('#g-l10n-search-form').submit(function() { + return false; + }); + + // Custom listener for l10n_client livesearch + $('#l10n-client #g-l10n-search').keyup(function(key) { + Gallery.l10nClient.filter($('#l10n-client #g-l10n-search').val()); + }); + + // Clear search + $('#l10n-client #l10n-search-filter-clear').click(function() { + Gallery.l10nClient.filter(false); + return false; + }); + + // Send AJAX POST data on form submit. + $('#g-l10n-client-save-form').ajaxForm({ + dataType: "json", + success: function(data) { + var source = Gallery.l10nClient.getString(Gallery.l10nClient.selected, 'source'); + var is_plural = Gallery.l10nClient.isPluralMessage(source); + var num_plural_forms = plural_forms.length; + + // Store translation in local js + var translation = {}; + var is_non_empty = false; + if (is_plural) { + for (var i = 0; i < num_plural_forms; i++) { + var form = plural_forms[i]; + translation[form] = $("#plural-" + form + " textarea[name='l10n-edit-plural-translation-" + form + "']").attr('value'); + is_non_empty = is_non_empty || translation[form]; + } + } else { + translation = $('#l10n-edit-translation').attr('value'); + is_non_empty = translation; + } + Gallery.l10nClient.setString(Gallery.l10nClient.selected, translation); + + // Mark message as translated / untranslated. + var source_element = $('#l10n-client-string-select li').eq(Gallery.l10nClient.selected); + if (is_non_empty) { + source_element.removeClass('untranslated').removeClass('active').addClass('translated'); + } else { + source_element.removeClass('active').removeClass('translated').addClass('untranslated'); + } + + // Clear the translation form fields + Gallery.l10nClient.showSourceMessage('', false); + $('#g-l10n-client-save-form #l10n-edit-translation').val(''); + + for (var i = 0; i < num_plural_forms; i++) { + var form = plural_forms[i]; + $("#plural-" + form + " textarea[name='l10n-edit-plural-translation-" + form + "']").val(''); + } + $("#g-l10n-client-save-form input[name='l10n-message-key']").val(''); + }, + error: function(xmlhttp) { + // TODO: Localize this message + alert('An HTTP error @status occured (or empty response).'.replace('@status', xmlhttp.status)); + } + }); + + // TODO: Add copy/clear buttons (without ajax behavior) + /* <input type="submit" name="l10n-edit-copy" value="<?= t("Copy source") ?>"/> + <input type="submit" name="l10n-edit-clear" value="<?= t("Clear") ?>"/> + */ + // TODO: Handle plurals in copy button + + // Copy source text to translation field on button click. + $('#g-l10n-client-save-form #l10n-edit-copy').click(function() { + $('#g-l10n-client-save-form #l10n-edit-target').val($('#l10n-client-string-editor .source-text').text()); + }); + + // Clear translation field on button click. + $('#g-l10n-client-save-form #l10n-edit-clear').click(function() { + $('#g-l10n-client-save-form #l10n-edit-target').val(''); + }); +}; diff --git a/modules/gallery/libraries/Admin_View.php b/modules/gallery/libraries/Admin_View.php new file mode 100644 index 0000000..62645d1 --- /dev/null +++ b/modules/gallery/libraries/Admin_View.php @@ -0,0 +1,120 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_View_Core extends Gallery_View { + /** + * Attempts to load a view and pre-load view data. + * + * @throws Kohana_Exception if the requested view cannot be found + * @param string $name view name + * @param string $theme_name view name + * @return void + */ + public function __construct($name) { + parent::__construct($name); + + $this->theme_name = module::get_var("gallery", "active_admin_theme"); + if (identity::active_user()->admin) { + $theme_name = Input::instance()->get("theme"); + if ($theme_name && + file_exists(THEMEPATH . $theme_name) && + strpos(realpath(THEMEPATH . $theme_name), THEMEPATH) == 0) { + $this->theme_name = $theme_name; + } + } + $this->sidebar = ""; + $this->set_global(array("theme" => $this, + "user" => identity::active_user(), + "page_type" => "admin", + "page_subtype" => $name, + "page_title" => null)); + } + + public function admin_menu() { + $menu = Menu::factory("root"); + module::event("admin_menu", $menu, $this); + + $settings_menu = $menu->get("settings_menu"); + uasort($settings_menu->elements, array("Menu", "title_comparator")); + + return $menu->render(); + } + + public function user_menu() { + $menu = Menu::factory("root") + ->css_id("g-login-menu") + ->css_class("g-inline ui-helper-clear-fix"); + module::event("user_menu", $menu, $this); + return $menu->render(); + } + + /** + * Print out any site wide status information. + */ + public function site_status() { + return site_status::get(); + } + + /** + * Print out any messages waiting for this user. + */ + public function messages() { + return message::get(); + } + + /** + * Handle all theme functions that insert module content. + */ + public function __call($function, $args) { + switch ($function) { + case "admin_credits"; + case "admin_footer": + case "admin_header_top": + case "admin_header_bottom": + case "admin_page_bottom": + case "admin_page_top": + case "admin_head": + case "body_attributes": + case "html_attributes": + $blocks = array(); + foreach (module::active() as $module) { + $helper_class = "{$module->name}_theme"; + if (class_exists($helper_class) && method_exists($helper_class, $function)) { + $blocks[] = call_user_func_array( + array($helper_class, $function), + array_merge(array($this), $args)); + } + } + + if (Session::instance()->get("debug")) { + if ($function != "admin_head") { + array_unshift( + $blocks, "<div class=\"g-annotated-theme-block g-annotated-theme-block_$function\">" . + "<div class=\"title\">$function</div>"); + $blocks[] = "</div>"; + } + } + + return implode("\n", $blocks); + + default: + throw new Exception("@todo UNKNOWN_THEME_FUNCTION: $function"); + } + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/Block.php b/modules/gallery/libraries/Block.php new file mode 100644 index 0000000..818af9c --- /dev/null +++ b/modules/gallery/libraries/Block.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Block_Core { + public $content = null; + public $css_id = null; + public $id = null; + public $title = null; + public $anchor = null; + + public function __toString() { + return View::factory("block.html", get_object_vars($this))->__toString(); + } +} diff --git a/modules/gallery/libraries/Breadcrumb.php b/modules/gallery/libraries/Breadcrumb.php new file mode 100644 index 0000000..3805c5d --- /dev/null +++ b/modules/gallery/libraries/Breadcrumb.php @@ -0,0 +1,70 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Breadcrumb_Core { + public $title; + public $url; + public $first; + public $last; + + static function instance($title, $url) { + return new Breadcrumb($title, $url); + } + + public function __construct($title, $url) { + $this->title = $title; + $this->url = $url; + $this->first = false; + $this->last = false; + } + + /** + * Return an array of Breadcrumb instances build from the parents of a given item. + * The first and last Breadcrumb instances will be marked first/last as appropriate. + * Each breadcrumb will have a ?show= query parameter that refers to the id of the next + * item in line. + * + * @return array Breadcrumb instances + */ + static function array_from_item_parents($item) { + if ($item->id == item::root()->id) { + return array(); + } + + $bc = array_merge($item->parents()->as_array(), array($item)); + for ($i = 0; $i < count($bc) - 1; $i++) { + $bc[$i] = new Breadcrumb($bc[$i]->title, $bc[$i]->url("show={$bc[$i+1]->id}")); + } + $bc[$i] = new Breadcrumb($item->title, $item->url()); + + $bc[0]->set_first(); + end($bc)->set_last(); + return $bc; + } + + public function set_first() { + $this->first = true; + return $this; + } + + public function set_last() { + $this->last = true; + return $this; + } +} diff --git a/modules/gallery/libraries/Form_Script.php b/modules/gallery/libraries/Form_Script.php new file mode 100644 index 0000000..2b5ec3e --- /dev/null +++ b/modules/gallery/libraries/Form_Script.php @@ -0,0 +1,66 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Form_Script_Core extends Forge { + protected $data = array( + "name" => false, + "type" => "script", + "url" => "", + "text" => ""); + + public function __construct($name) { + // Set dummy data so we don"t get errors + $this->attr["action"] = ""; + $this->attr["method"] = "post"; + $this->data["name"] = $name; + } + + public function __get($key) { + return isset($this->data[$key]) ? $this->data[$key] : null; + } + + /** + * Sets url attribute + */ + public function url($url) { + $this->data["url"] = $url; + + return $this; + } + + public function text($script_text) { + $this->data["text"] = $script_text; + + return $this; + } + + public function render($template="forge_template", $custom=false) { + $script = array(); + if (!empty($this->data["url"])) { + $script[] = html::script($this->data["url"]); + } + + if (!empty($this->data["text"])) { + $script[] = "<script type=\"text/javascript\">\n{$this->data['text']}\n</script>\n"; + } + + return implode("\n", $script); + } + +}
\ No newline at end of file diff --git a/modules/gallery/libraries/Form_Uploadify.php b/modules/gallery/libraries/Form_Uploadify.php new file mode 100644 index 0000000..1e58018 --- /dev/null +++ b/modules/gallery/libraries/Form_Uploadify.php @@ -0,0 +1,72 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Form_Uploadify_Core extends Form_Input { + protected $data = array( + "name" => false, + "type" => "UNKNOWN", + "url" => "", + "text" => ""); + + public function __construct($name) { + parent::__construct($name); + $this->data["script_data"] = array( + "g3sid" => Session::instance()->id(), + "user_agent" => Input::instance()->server("HTTP_USER_AGENT"), + "csrf" => access::csrf_token()); + } + + public function album(Item_Model $album) { + $this->data["album"] = $album; + return $this; + } + + public function script_data($key, $value) { + $this->data["script_data"][$key] = $value; + } + + public function render() { + $v = new View("form_uploadify.html"); + $v->album = $this->data["album"]; + $v->script_data = $this->data["script_data"]; + $v->simultaneous_upload_limit = module::get_var("gallery", "simultaneous_upload_limit"); + $v->movies_allowed = movie::allow_uploads(); + $v->extensions = legal_file::get_filters(); + $v->suhosin_session_encrypt = (bool) ini_get("suhosin.session.encrypt"); + + list ($toolkit_max_filesize_bytes, $toolkit_max_filesize) = graphics::max_filesize(); + + $upload_max_filesize = trim(ini_get("upload_max_filesize")); + $upload_max_filesize_bytes = num::convert_to_bytes($upload_max_filesize); + + if ($upload_max_filesize_bytes < $toolkit_max_filesize_bytes) { + $v->size_limit_bytes = $upload_max_filesize_bytes; + $v->size_limit = $upload_max_filesize; + } else { + $v->size_limit_bytes = $toolkit_max_filesize_bytes; + $v->size_limit = $toolkit_max_filesize; + } + + return $v; + } + + public function validate() { + return true; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/Form_Uploadify_buttons.php b/modules/gallery/libraries/Form_Uploadify_buttons.php new file mode 100644 index 0000000..2e0327e --- /dev/null +++ b/modules/gallery/libraries/Form_Uploadify_buttons.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Form_Uploadify_buttons_Core extends Form_Input { + public function render() { + $v = new View("form_uploadify_buttons.html"); + return $v; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/Gallery_I18n.php b/modules/gallery/libraries/Gallery_I18n.php new file mode 100644 index 0000000..6b216a2 --- /dev/null +++ b/modules/gallery/libraries/Gallery_I18n.php @@ -0,0 +1,472 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Translates a localizable message. + * @param $message String The message to be translated. E.g. "Hello world" + * @param $options array (optional) Options array for key value pairs which are used + * for pluralization and interpolation. Special key: "locale" to override the + * currently configured locale. + * @return String The translated message string. + */ +function t($message, $options=array()) { + return Gallery_I18n::instance()->translate($message, $options); +} + +/** + * Translates a localizable message with plural forms. + * @param $singular String The message to be translated. E.g. "There is one album." + * @param $plural String The plural message to be translated. E.g. + * "There are %count albums." + * @param $count Number The number which is inserted for the %count placeholder and + * which is used to select the proper plural form ($singular or $plural). + * @param $options array (optional) Options array for key value pairs which are used + * for pluralization and interpolation. Special key: "locale" to override the + * currently configured locale. + * @return String The translated message string. + */ +function t2($singular, $plural, $count, $options=array()) { + return Gallery_I18n::instance()->translate(array("one" => $singular, "other" => $plural), + array_merge($options, array("count" => $count))); +} + +class Gallery_I18n_Core { + private static $_instance; + private $_config = array(); + private $_call_log = array(); + private $_cache = array(); + + private function __construct($config) { + $this->_config = $config; + $this->locale($config['default_locale']); + } + + public static function instance($config=null) { + if (self::$_instance == NULL || isset($config)) { + $config = isset($config) ? $config : Kohana::config('locale'); + if (empty($config['default_locale'])) { + $config['default_locale'] = module::get_var('gallery', 'default_locale'); + } + self::$_instance = new Gallery_I18n_Core($config); + } + + return self::$_instance; + } + + public function locale($locale=null) { + if ($locale) { + $this->_config['default_locale'] = $locale; + $php_locale = setlocale(LC_ALL, 0); + list ($php_locale, $unused) = explode('.', $php_locale . '.'); + if ($php_locale != $locale) { + // Attempt to set PHP's locale as well (for number formatting, collation, etc.) + $locale_prefs = array($locale); + // Try appending some character set names; some systems (like FreeBSD) need this. + // Some systems require a format with hyphen (eg. Gentoo) and others without (eg. FreeBSD). + $charsets = array('utf8', 'UTF-8', 'UTF8', 'ISO8859-1', 'ISO-8859-1'); + if (substr($locale, 0, 2) != 'en') { + $charsets = array_merge($charsets, array( + 'EUC', 'Big5', 'euc', 'ISO8859-2', 'ISO8859-5', 'ISO8859-7', + 'ISO8859-9', 'ISO-8859-2', 'ISO-8859-5', 'ISO-8859-7', 'ISO-8859-9')); + } + foreach ($charsets as $charset) { + $locale_prefs[] = $locale . '.' . $charset; + } + $locale_prefs[] = 'en_US'; + $php_locale = setlocale(LC_ALL, $locale_prefs); + } + if (is_string($php_locale) && substr($php_locale, 0, 2) == 'tr') { + // Make PHP 5 work with Turkish (the localization results are mixed though). + // Hack for http://bugs.php.net/18556 + setlocale(LC_CTYPE, 'C'); + } + } + 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. + * + * Security: + * The returned string is safe for use in HTML (it contains a safe subset of HTML and + * interpolation parameters are converted to HTML entities). + * For use in JavaScript, please call ->for_js() on it. + * + * @param $message String|array The message to be translated. E.g. "Hello world" + * or array("one" => "One album", "other" => "%count albums") + * @param $options array (optional) Options array for key value pairs which are used + * for pluralization and interpolation. Special keys are "count" and "locale", + * the latter to override the currently configured locale. + * @return String The translated message string. + */ + public function translate($message, $options=array()) { + $locale = empty($options['locale']) ? $this->_config['default_locale'] : $options['locale']; + $count = isset($options['count']) ? $options['count'] : null; + $values = $options; + unset($values['locale']); + $this->log($message, $options); + + $entry = $this->lookup($locale, $message); + + if (null === $entry) { + // Default to the root locale. + $entry = $message; + $locale = $this->_config['root_locale']; + } + + $entry = $this->pluralize($locale, $entry, $count); + + $entry = $this->interpolate($locale, $entry, $values); + + return SafeString::of_safe_html($entry); + } + + private function lookup($locale, $message) { + if (!isset($this->_cache[$locale])) { + $this->_cache[$locale] = self::load_translations($locale); + } + + $key = self::get_message_key($message); + + if (isset($this->_cache[$locale][$key])) { + return $this->_cache[$locale][$key]; + } else { + return null; + } + } + + private static function load_translations($locale) { + $cache_key = "translation|" . $locale; + $cache = Cache::instance(); + $translations = $cache->get($cache_key); + if (!isset($translations) || !is_array($translations)) { + $translations = array(); + foreach (db::build() + ->select("key", "translation") + ->from("incoming_translations") + ->where("locale", "=", $locale) + ->execute() as $row) { + $translations[$row->key] = unserialize($row->translation); + } + + // Override incoming with outgoing... + foreach (db::build() + ->select("key", "translation") + ->from("outgoing_translations") + ->where("locale", "=", $locale) + ->execute() as $row) { + $translations[$row->key] = unserialize($row->translation); + } + + $cache->set($cache_key, $translations, array("translation"), 0); + } + return $translations; + } + + public function has_translation($message, $options=null) { + $locale = empty($options['locale']) ? $this->_config['default_locale'] : $options['locale']; + + $entry = $this->lookup($locale, $message); + + if (null === $entry) { + return false; + } else if (!is_array($message)) { + return $entry !== ''; + } else { + if (!is_array($entry) || empty($entry)) { + return false; + } + // It would be better to verify that all the locale's plural forms have a non-empty + // translation, but this is fine for now. + foreach ($entry as $value) { + if ($value === '') { + return false; + } + } + return true; + } + } + + static function get_message_key($message) { + $as_string = is_array($message) ? implode('|', $message) : $message; + return md5($as_string); + } + + static function is_plural_message($message) { + return is_array($message); + } + + private function interpolate($locale, $string, $key_values) { + // TODO: Handle locale specific number formatting. + + // Replace x_y before replacing x. + krsort($key_values, SORT_STRING); + + $keys = array(); + $values = array(); + foreach ($key_values as $key => $value) { + $keys[] = "%$key"; + $values[] = new SafeString($value); + } + return str_replace($keys, $values, $string); + } + + private function pluralize($locale, $entry, $count) { + if (!is_array($entry)) { + return $entry; + } + + $plural_key = self::get_plural_key($locale, $count); + if (!isset($entry[$plural_key])) { + // Fallback to the default plural form. + $plural_key = 'other'; + } + + if (isset($entry[$plural_key])) { + return $entry[$plural_key]; + } else { + // Fallback to just any plural form. + list ($plural_key, $string) = each($entry); + return $string; + } + } + + private function log($message, $options) { + $key = self::get_message_key($message); + isset($this->_call_log[$key]) or $this->_call_log[$key] = array($message, $options); + } + + public function call_log() { + return $this->_call_log; + } + + public static function clear_cache($locale=null) { + $cache = Cache::instance(); + if ($locale) { + $cache->delete("translation|" . $locale); + } else { + $cache->delete_tag("translation"); + } + } + + private static function get_plural_key($locale, $count) { + $parts = explode('_', $locale); + $language = $parts[0]; + + // Data from CLDR 1.6 (http://unicode.org/cldr/data/common/supplemental/plurals.xml). + // Docs: http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html + switch ($language) { + case 'az': + case 'fa': + case 'hu': + case 'ja': + case 'ko': + case 'my': + case 'to': + case 'tr': + case 'vi': + case 'yo': + case 'zh': + case 'bo': + case 'dz': + case 'id': + case 'jv': + case 'ka': + case 'km': + case 'kn': + case 'ms': + case 'th': + return 'other'; + + case 'ar': + if ($count == 0) { + return 'zero'; + } else if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else if (is_int($count) && ($i = $count % 100) >= 3 && $i <= 10) { + return 'few'; + } else if (is_int($count) && ($i = $count % 100) >= 11 && $i <= 99) { + return 'many'; + } else { + return 'other'; + } + + case 'pt': + case 'am': + case 'bh': + case 'fil': + case 'tl': + case 'guw': + case 'hi': + case 'ln': + case 'mg': + case 'nso': + case 'ti': + case 'wa': + if ($count == 0 || $count == 1) { + return 'one'; + } else { + return 'other'; + } + + case 'fr': + if ($count >= 0 and $count < 2) { + return 'one'; + } else { + return 'other'; + } + + case 'lv': + if ($count == 0) { + return 'zero'; + } else if ($count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else { + return 'other'; + } + + case 'ga': + case 'se': + case 'sma': + case 'smi': + case 'smj': + case 'smn': + case 'sms': + if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else { + return 'other'; + } + + case 'ro': + case 'mo': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && $count == 0 && ($i = $count % 100) >= 1 && $i <= 19) { + return 'few'; + } else { + return 'other'; + } + + case 'lt': + if (is_int($count) && $count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 9 && ($i = $count % 100) < 11 && $i > 19) { + return 'few'; + } else { + return 'other'; + } + + case 'hr': + case 'ru': + case 'sr': + case 'uk': + case 'be': + case 'bs': + case 'sh': + if (is_int($count) && $count % 10 == 1 && $count % 100 != 11) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 4 && ($i = $count % 100) < 12 && $i > 14) { + return 'few'; + } else if (is_int($count) && ($count % 10 == 0 || (($i = $count % 10) >= 5 && $i <= 9) || (($i = $count % 100) >= 11 && $i <= 14))) { + return 'many'; + } else { + return 'other'; + } + + case 'cs': + case 'sk': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && $count >= 2 && $count <= 4) { + return 'few'; + } else { + return 'other'; + } + + case 'pl': + if ($count == 1) { + return 'one'; + } else if (is_int($count) && ($i = $count % 10) >= 2 && $i <= 4 && + ($i = $count % 100) < 12 && $i > 14 && ($i = $count % 100) < 22 && $i > 24) { + return 'few'; + } else { + return 'other'; + } + + case 'sl': + if ($count % 100 == 1) { + return 'one'; + } else if ($count % 100 == 2) { + return 'two'; + } else if (is_int($count) && ($i = $count % 100) >= 3 && $i <= 4) { + return 'few'; + } else { + return 'other'; + } + + case 'mt': + if ($count == 1) { + return 'one'; + } else if ($count == 0 || is_int($count) && ($i = $count % 100) >= 2 && $i <= 10) { + return 'few'; + } else if (is_int($count) && ($i = $count % 100) >= 11 && $i <= 19) { + return 'many'; + } else { + return 'other'; + } + + case 'mk': + if ($count % 10 == 1) { + return 'one'; + } else { + return 'other'; + } + + case 'cy': + if ($count == 1) { + return 'one'; + } else if ($count == 2) { + return 'two'; + } else if ($count == 8 || $count == 11) { + return 'many'; + } else { + return 'other'; + } + + default: // en, de, etc. + return $count == 1 ? 'one' : 'other'; + } + } +} diff --git a/modules/gallery/libraries/Gallery_View.php b/modules/gallery/libraries/Gallery_View.php new file mode 100644 index 0000000..8f02b53 --- /dev/null +++ b/modules/gallery/libraries/Gallery_View.php @@ -0,0 +1,243 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Gallery_View_Core extends View { + protected $theme_name = null; + protected $combine_queue = array(); + + /** + * Provide a url to a resource within the current theme. This allows us to refer to theme + * resources without naming the theme itself which makes themes easier to copy. + */ + public function url($path, $absolute_url=false) { + $arg = "themes/{$this->theme_name}/$path"; + return $absolute_url ? url::abs_file($arg) : url::file($arg); + } + + /** + * Set up the data and render a pager. + * + * See themes/wind/views/pager.html for documentation on the variables generated here. + */ + public function paginator() { + $v = new View("paginator.html"); + $v->page_type = $this->page_type; + $v->page_subtype = $this->page_subtype; + $v->first_page_url = null; + $v->previous_page_url = null; + $v->next_page_url = null; + $v->last_page_url = null; + + if ($this->page_type == "collection") { + $v->page = $this->page; + $v->max_pages = $this->max_pages; + $v->total = $this->children_count; + + if ($this->page != 1) { + $v->first_page_url = url::site(url::merge(array("page" => 1))); + $v->previous_page_url = url::site(url::merge(array("page" => $this->page - 1))); + } + + if ($this->page != $this->max_pages) { + $v->next_page_url = url::site(url::merge(array("page" => $this->page + 1))); + $v->last_page_url = url::site(url::merge(array("page" => $this->max_pages))); + } + + $v->first_visible_position = ($this->page - 1) * $this->page_size + 1; + $v->last_visible_position = min($this->page * $this->page_size, $v->total); + } else if ($this->page_type == "item") { + $v->position = $this->position; + $v->total = $this->sibling_count; + if ($this->previous_item) { + $v->previous_page_url = $this->previous_item->url(); + } + + if ($this->next_item) { + $v->next_page_url = $this->next_item->url(); + } + } + + return $v; + } + + /** + * Begin gather up scripts or css files so that they can be combined into a single request. + * + * @param $types a comma separated list of types to combine, eg "script,css" + */ + public function start_combining($types) { + if (gallery::allow_css_and_js_combining()) { + foreach (explode(",", $types) as $type) { + $this->combine_queue[$type] = array(); + } + } + } + + /** + * If script combining is enabled, add this script to the list of scripts that will be + * combined into a single script element. When combined, the order of scripts is preserved. + * + * @param $file the file name or path of the script to include. If a path is specified then + * it needs to be relative to DOCROOT. Just specifying a file name will result + * in searching Kohana's cascading file system. + * @param $group the group of scripts to combine this with. defaults to "core" + */ + public function script($file, $group="core") { + if (($path = gallery::find_file("js", $file, false))) { + if (isset($this->combine_queue["script"])) { + $this->combine_queue["script"][$group][$path] = 1; + } else { + return html::script($path); + } + } else { + Kohana_Log::add("error", "Can't find script file: $file"); + } + } + + /** + * If css combining is enabled, add this css to the list of css that will be + * combined into a single style element. When combined, the order of style elements + * is preserved. + * + * @param $file the file name or path of the css to include. If a path is specified then + * it needs to be relative to DOCROOT. Just specifying a file name will result + * in searching Kohana's cascading file system. + * @param $group the group of css to combine this with. defaults to "core" + */ + public function css($file, $group="core") { + if (($path = gallery::find_file("css", $file, false))) { + if (isset($this->combine_queue["css"])) { + $this->combine_queue["css"][$group][$path] = 1; + } else { + return html::stylesheet($path); + } + } else { + Kohana_Log::add("error", "Can't find css file: $file"); + } + } + + /** + * Combine a series of files into a single one and cache it in the database. + * @param $type the data type (script or css) + * @param $group the group of scripts or css we want + */ + public function get_combined($type, $group="core") { + $links = array(); + + if (empty($this->combine_queue[$type][$group])) { + return; + } + + // Include the url in the cache key so that if the Gallery moves, we don't use old cached + // entries. + $key = array(url::abs_file("")); + + foreach (array_keys($this->combine_queue[$type][$group]) as $path) { + $stats = stat($path); + // 7 == size, 9 == mtime, see http://php.net/stat + $key[] = "$path $stats[7] $stats[9]"; + } + + $key = md5(join(" ", $key)); + $cache = Cache::instance(); + $contents = $cache->get($key); + + if (empty($contents)) { + $combine_data = new stdClass(); + $combine_data->type = $type; + $combine_data->contents = $this->combine_queue[$type][$group]; + module::event("before_combine", $combine_data); + + $contents = ""; + foreach (array_keys($this->combine_queue[$type][$group]) as $path) { + if ($type == "css") { + $contents .= "/* $path */\n" . $this->process_css($path) . "\n"; + } else { + $contents .= "/* $path */\n" . file_get_contents($path) . "\n"; + } + } + + $combine_data = new stdClass(); + $combine_data->type = $type; + $combine_data->contents = $contents; + module::event("after_combine", $combine_data); + + $cache->set($key, $combine_data->contents, array($type), 30 * 84600); + + $use_gzip = function_exists("gzencode") && + (int) ini_get("zlib.output_compression") === 0; + if ($use_gzip) { + $cache->set("{$key}_gz", gzencode($combine_data->contents, 9, FORCE_GZIP), + array($type, "gzip"), 30 * 84600); + } + + } + + unset($this->combine_queue[$type][$group]); + if (empty($this->combine_queue[$type])) { + unset($this->combine_queue[$type]); + } + + if ($type == "css") { + return html::stylesheet("combined/css/$key", "screen,print,projection", true); + } else { + return html::script("combined/javascript/$key", true); + } + } + + /** + * Convert relative references inside a CSS file to absolute ones so that when it's served from + * a new location as part of a combined bundle the references are still correct. + * @param string the path to the css file + */ + private function process_css($css_file) { + static $PATTERN = "#url\(\s*['|\"]{0,1}(.*?)['|\"]{0,1}\s*\)#"; + $docroot_length = strlen(DOCROOT); + + $css = file_get_contents($css_file); + if (preg_match_all($PATTERN, $css, $matches, PREG_SET_ORDER)) { + $search = $replace = array(); + foreach ($matches as $match) { + $relative = dirname($css_file) . "/$match[1]"; + if (!empty($relative)) { + $search[] = $match[0]; + $replace[] = "url('" . url::abs_file($relative) . "')"; + } else { + Kohana_Log::add("error", "Missing URL reference '{$match[1]}' in CSS file '$css_file'"); + + } + } + $replace = str_replace(DIRECTORY_SEPARATOR, "/", $replace); + $css = str_replace($search, $replace, $css); + } + $imports = preg_match_all("#@import\s*['|\"]{0,1}(.*?)['|\"]{0,1};#", + $css, $matches, PREG_SET_ORDER); + + if ($imports) { + $search = $replace = array(); + foreach ($matches as $match) { + $search[] = $match[0]; + $replace[] = $this->process_css(dirname($css_file) . "/$match[1]"); + } + $css = str_replace($search, $replace, $css); + } + + return $css; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/IdentityProvider.php b/modules/gallery/libraries/IdentityProvider.php new file mode 100644 index 0000000..525e169 --- /dev/null +++ b/modules/gallery/libraries/IdentityProvider.php @@ -0,0 +1,283 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Provides a driver-based interface for managing users and groups. + */ +class IdentityProvider_Core { + protected static $instance; + + // Configuration + protected $config; + + // Driver object + protected $driver; + + /** + * Returns a singleton instance of Identity. + * There can only be one Identity driver configured at a given point + * + * @param string configuration + * @return Identity_Core + */ + static function &instance() { + if (empty(self::$instance)) { + // Create a new instance + self::$instance = new IdentityProvider(); + } + + return self::$instance; + } + + /** + * Frees the current instance of the identity provider so the next call to instance will reload + * + * @param string configuration + * @return Identity_Core + */ + static function reset() { + self::$instance = null; + Kohana_Config::instance()->clear("identity"); + } + + /** + * Return a commen confirmation message + */ + static function confirmation_message() { + return t("Are you sure you want to change your Identity Provider? Continuing will delete all existing users."); + } + + static function change_provider($new_provider) { + if (!identity::active_user()->admin && PHP_SAPI != "cli") { + // Below, the active user is set to the primary admin. + access::forbidden(); + } + + $current_provider = module::get_var("gallery", "identity_provider"); + if (!empty($current_provider)) { + module::uninstall($current_provider); + } + + try { + IdentityProvider::reset(); + $provider = new IdentityProvider($new_provider); + + module::set_var("gallery", "identity_provider", $new_provider); + + if (class_exists("{$new_provider}_installer") && + method_exists("{$new_provider}_installer", "initialize")) { + call_user_func("{$new_provider}_installer::initialize"); + } + + if (!$provider->admin_user()) { + throw new Exception("IdentityProvider $new_provider: Couldn't find the admin user!"); + } + + module::event("identity_provider_changed", $current_provider, $new_provider); + + identity::set_active_user($provider->admin_user()); + Session::instance()->regenerate(); + } catch (Exception $e) { + static $restore_already_running; + + // In case of error, make an attempt to restore the old provider. Since that's calling into + // this function again and can fail, we should be sure not to get into an infinite recursion. + if (!$restore_already_running) { + $restore_already_running = true; + + // Make sure new provider is not in the database + try { + module::uninstall($new_provider); + } catch (Exception $e2) { + Kohana_Log::add("error", "Error uninstalling failed new provider\n" . + $e2->getMessage() . "\n" . $e2->getTraceAsString()); + } + + try { + // Lets reset to the current provider so that the gallery installation is still + // working. + module::set_var("gallery", "identity_provider", null); + IdentityProvider::change_provider($current_provider); + module::activate($current_provider); + } catch (Exception $e2) { + Kohana_Log::add("error", "Error restoring original identity provider\n" . + $e2->getMessage() . "\n" . $e2->getTraceAsString()); + } + + message::error( + t("Error attempting to enable \"%new_provider\" identity provider, reverted to \"%old_provider\" identity provider", + array("new_provider" => $new_provider, "old_provider" => $current_provider))); + + $restore_already_running = false; + } + throw $e; + } + } + + /** + * Loads the configured driver and validates it. + * + * @return void + */ + public function __construct($config=null) { + if (empty($config)) { + $config = module::get_var("gallery", "identity_provider", "user"); + } + + // Test the config group name + if (($this->config = Kohana::config("identity." . $config)) === NULL) { + throw new Exception("@todo NO_USER_LIBRARY_CONFIGURATION_FOR: $config"); + } + + // Set driver name + $driver = "IdentityProvider_" . ucfirst($this->config["driver"]) ."_Driver"; + + // Load the driver + if ( ! Kohana::auto_load($driver)) { + throw new Kohana_Exception("core.driver_not_found", $this->config["driver"], + get_class($this)); + } + + // Initialize the driver + $this->driver = new $driver($this->config["params"]); + + // Validate the driver + if ( !($this->driver instanceof IdentityProvider_Driver)) { + throw new Kohana_Exception("core.driver_implements", $this->config["driver"], + get_class($this), "IdentityProvider_Driver"); + } + + Kohana_Log::add("debug", "Identity Library initialized"); + } + + /** + * Determine if if the current driver supports updates. + * + * @return boolean true if the driver supports updates; false if read only + */ + public function is_writable() { + return !empty($this->config["allow_updates"]); + } + + /** + * @see IdentityProvider_Driver::guest. + */ + public function guest() { + return $this->driver->guest(); + } + + /** + * @see IdentityProvider_Driver::admin_user. + */ + public function admin_user() { + return $this->driver->admin_user(); + } + + /** + * @see IdentityProvider_Driver::create_user. + */ + public function create_user($name, $full_name, $password, $email) { + return $this->driver->create_user($name, $full_name, $password, $email); + } + + /** + * @see IdentityProvider_Driver::is_correct_password. + */ + public function is_correct_password($user, $password) { + return $this->driver->is_correct_password($user, $password); + } + + /** + * @see IdentityProvider_Driver::lookup_user. + */ + public function lookup_user($id) { + return $this->driver->lookup_user($id); + } + + /** + * @see IdentityProvider_Driver::lookup_user_by_name. + */ + public function lookup_user_by_name($name) { + return $this->driver->lookup_user_by_name($name); + } + + /** + * @see IdentityProvider_Driver::create_group. + */ + public function create_group($name) { + return $this->driver->create_group($name); + } + + /** + * @see IdentityProvider_Driver::everybody. + */ + public function everybody() { + return $this->driver->everybody(); + } + + /** + * @see IdentityProvider_Driver::registered_users. + */ + public function registered_users() { + return $this->driver->registered_users(); + } + + /** + * @see IdentityProvider_Driver::lookup_group. + */ + public function lookup_group($id) { + return $this->driver->lookup_group($id); + } + + /** + * @see IdentityProvider_Driver::lookup_group_by_name. + */ + public function lookup_group_by_name($name) { + return $this->driver->lookup_group_by_name($name); + } + + /** + * @see IdentityProvider_Driver::get_user_list. + */ + public function get_user_list($ids) { + return $this->driver->get_user_list($ids); + } + + /** + * @see IdentityProvider_Driver::groups. + */ + public function groups() { + return $this->driver->groups(); + } + + /** + * @see IdentityProvider_Driver::add_user_to_group. + */ + public function add_user_to_group($user, $group) { + return $this->driver->add_user_to_group($user, $group); + } + + /** + * @see IdentityProvider_Driver::remove_user_to_group. + */ + public function remove_user_from_group($user, $group) { + return $this->driver->remove_user_from_group($user, $group); + } +} // End Identity diff --git a/modules/gallery/libraries/InPlaceEdit.php b/modules/gallery/libraries/InPlaceEdit.php new file mode 100644 index 0000000..cd177c2 --- /dev/null +++ b/modules/gallery/libraries/InPlaceEdit.php @@ -0,0 +1,91 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class InPlaceEdit_Core { + private $rules = array(); + private $messages = array(); + private $callback = array(); + private $initial_value; + private $action = ""; + private $errors; + private $form; + + static function factory($initial_value) { + $instance = new InPlaceEdit(); + $instance->initial_value = $initial_value; + $instance->form = array("input" => $initial_value); + $instance->errors = array("input" => ""); + + return $instance; + } + + public function action($action) { + $this->action = $action; + return $this; + } + + public function rules($rules) { + $this->rules += $rules; + return $this; + } + + public function messages($messages) { + $this->messages += $messages; + return $this; + } + + public function callback($callback) { + $this->callback = $callback; + return $this; + } + + public function validate() { + $post = Validation::factory($_POST); + + if (!empty($this->callback)) { + $post->add_callbacks("input", $this->callback); + } + + foreach ($this->rules as $rule) { + $post->add_rules("input", $rule); + } + + $valid = $post->validate(); + $this->form = array_merge($this->form, $post->as_array()); + $this->errors = array_merge($this->errors, $post->errors()); + return $valid; + } + + public function render() { + $v = new View("in_place_edit.html"); + $v->action = $this->action; + $v->form = $this->form; + $v->errors = $this->errors; + foreach ($v->errors as $key => $error) { + if (!empty($error)) { + $v->errors[$key] = $this->messages[$error]; + } + } + return $v->render(); + } + + public function value() { + return $this->form["input"]; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/MY_Database.php b/modules/gallery/libraries/MY_Database.php new file mode 100644 index 0000000..33759b6 --- /dev/null +++ b/modules/gallery/libraries/MY_Database.php @@ -0,0 +1,101 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +abstract class Database extends Database_Core { + protected $_table_names; + + /** + * Kohana 2.4 introduces a new connection parameter. If it's not specified, make sure that we + * define it here to avoid an error later on. + * + * @todo: add an upgrade path to modify var/database.php so that we can avoid doing this at + * runtime. + */ + protected function __construct(array $config) { + if (!isset($config["connection"]["params"])) { + $config["connection"]["params"] = null; + } + parent::__construct($config); + if (gallery::show_profiler()) { + $this->config['benchmark'] = true; + } + } + + /** + * Parse the query string and convert any strings of the form `\([a-zA-Z0-9_]*?)\] + * table prefix . $1 + */ + public function query($sql) { + if (!empty($sql)) { + $sql = $this->add_table_prefixes($sql); + } + return parent::query($sql); + } + + public function add_table_prefixes($sql) { + $prefix = $this->config["table_prefix"]; + if (strpos($sql, "SHOW TABLES") === 0) { + /* + * Don't ignore "show tables", otherwise we could have a infinite + * @todo this may have to be changed if we support more than mysql + */ + return $sql; + } else if (strpos($sql, "CREATE TABLE") === 0) { + // Creating a new table; add it to the table cache. + $open_brace = strpos($sql, "{") + 1; + $close_brace = strpos($sql, "}", $open_brace); + $name = substr($sql, $open_brace, $close_brace - $open_brace); + $this->_table_names["{{$name}}"] = "`{$prefix}$name`"; + } 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)) { + // This should only run once on the first query + $this->_table_names = array(); + foreach($this->list_tables() as $table_name) { + $this->_table_names["{{$table_name}}"] = "`{$prefix}{$table_name}`"; + } + } + + return strtr($sql, $this->_table_names); + } + + /** + * This is used by the unit test code to switch the active database connection. + */ + static function set_default_instance($db) { + self::$instances["default"] = $db; + } + + /** + * Escape LIKE queries, add wildcards. In MySQL queries using LIKE, _ and % characters are + * treated as wildcards similar to ? and *, respectively. Therefore, we need to escape _, %, + * and \ (the escape character itself). + */ + static function escape_for_like($value) { + // backslash must go first to avoid double-escaping + return addcslashes($value, '\_%'); + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/MY_Forge.php b/modules/gallery/libraries/MY_Forge.php new file mode 100644 index 0000000..635dc2d --- /dev/null +++ b/modules/gallery/libraries/MY_Forge.php @@ -0,0 +1,45 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class Forge extends Forge_Core { + /** + * Force a CSRF element into every form. + */ + public function __construct($action=null, $title='', $method=null, $attr=array()) { + parent::__construct($action, $title, $method, $attr); + $this->hidden("csrf")->value(access::csrf_token()); + } + + /** + * Use our own template + */ + public function render($template="form.html", $custom=false) { + return parent::render($template, $custom); + } + + /** + * Validate our CSRF value as a mandatory part of all form validation. + */ + public function validate() { + $status = parent::validate(); + access::verify_csrf(); + return $status; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/MY_Input.php b/modules/gallery/libraries/MY_Input.php new file mode 100644 index 0000000..2f0a727 --- /dev/null +++ b/modules/gallery/libraries/MY_Input.php @@ -0,0 +1,31 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class 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('#[^a-zA-Z0-9:_.-]+#', '_', $str); + } +} diff --git a/modules/gallery/libraries/MY_Kohana_Exception.php b/modules/gallery/libraries/MY_Kohana_Exception.php new file mode 100644 index 0000000..51490a6 --- /dev/null +++ b/modules/gallery/libraries/MY_Kohana_Exception.php @@ -0,0 +1,101 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Kohana_Exception extends Kohana_Exception_Core { + /** + * Dump out the full stack trace as part of the text representation of the exception. + */ + public static function text($e) { + if ($e instanceof Kohana_404_Exception) { + return "File not found: " . rawurlencode(Router::$complete_uri); + } else { + return sprintf( + "%s [ %s ]: %s\n%s [ %s ]\n%s", + get_class($e), $e->getCode(), strip_tags($e->getMessage()), + $e->getFile(), $e->getLine(), + $e->getTraceAsString()); + } + } + + /** + * @see Kohana_Exception::dump() + */ + public static function dump($value, $length=128, $max_level=5) { + return self::safe_dump($value, null, $length, $max_level); + } + + /** + * A safer version of dump(), eliding sensitive information in the dumped + * data, such as session ids and passwords / hashes. + */ + public static function safe_dump($value, $key, $length=128, $max_level=5) { + return parent::dump(self::_sanitize_for_dump($value, $key, $max_level), $length, $max_level); + } + + /** + * Elides sensitive data which shouldn't be echoed to the client, + * such as passwords, and other secrets. + */ + /* Visible for testing*/ static function _sanitize_for_dump($value, $key=null, $max_level) { + // Better elide too much than letting something through. + // Note: unanchored match is intended. + if (!$max_level) { + // Too much recursion; give up. We gave it our best shot. + return $value; + } + + $sensitive_info_pattern = + '/(password|pass|email|hash|private_key|session_id|session|g3sid|csrf|secret)/i'; + if (preg_match($sensitive_info_pattern, $key) || + (is_string($value) && preg_match('/[a-f0-9]{20,}/i', $value))) { + return 'removed for display'; + } else if (is_object($value)) { + if ($value instanceof Database) { + // Elide database password, host, name, user, etc. + return get_class($value) . ' object - details omitted for display'; + } else if ($value instanceof User_Model) { + return get_class($value) . ' object for "' . $value->name . '" - details omitted for display'; + } + return self::_sanitize_for_dump((array) $value, $key, $max_level - 1); + } else if (is_array($value)) { + $result = array(); + foreach ($value as $k => $v) { + $actual_key = $k; + $key_for_display = $k; + if ($k[0] === "\x00") { + // Remove the access level from the variable name + $actual_key = substr($k, strrpos($k, "\x00") + 1); + $access = $k[1] === '*' ? 'protected' : 'private'; + $key_for_display = "$access: $actual_key"; + } + if (is_object($v)) { + $key_for_display .= ' (type: ' . get_class($v) . ')'; + } + $result[$key_for_display] = self::_sanitize_for_dump($v, $actual_key, $max_level - 1); + } + } else { + $result = $value; + } + return $result; + } + + public static function debug_path($file) { + return html::clean(parent::debug_path($file)); + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/MY_ORM.php b/modules/gallery/libraries/MY_ORM.php new file mode 100644 index 0000000..6e538d5 --- /dev/null +++ b/modules/gallery/libraries/MY_ORM.php @@ -0,0 +1,52 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class ORM extends ORM_Core { + + /** + * Make sure that we're only using integer ids. + */ + static function factory($model, $id=null) { + if ($id && !is_int($id) && !is_string($id)) { + throw new Exception("@todo ORM::factory requires integer ids"); + } + return ORM_Core::factory($model, (int) $id); + } + + public function save() { + model_cache::clear(); + return parent::save(); + } +} + +/** + * Slide this in here for convenience. We won't ever be overloading ORM_Iterator without ORM. + */ +class ORM_Iterator extends ORM_Iterator_Core { + /** + * Cache the result row + */ + public function current() { + $row = parent::current(); + if (is_object($row)) { + model_cache::set($row); + } + return $row; + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/MY_View.php b/modules/gallery/libraries/MY_View.php new file mode 100644 index 0000000..1e88611 --- /dev/null +++ b/modules/gallery/libraries/MY_View.php @@ -0,0 +1,85 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class View extends View_Core { + static $global_data = array(); + + /** + * Reimplement Kohana 2.3's View::set_global() functionality. + */ + public function set_global($key, $value = NULL) { + if (is_array($key)) { + foreach ($key as $key2 => $value) { + View::$global_data[$key2] = $value; + } + } else { + View::$global_data[$key] = $value; + } + } + + public function is_set($key=null) { + return parent::is_set($key) ? true : array_key_exists($key, View::$global_data); + } + + /** + * Completely replace View_Core::__get() so that local data trumps global data, trumps members. + * This simulates the Kohana 2.3 behavior. + */ + public function &__get($key) { + if (isset($this->kohana_local_data[$key])) { + return $this->kohana_local_data[$key]; + } else if (isset(View::$global_data[$key])) { + return View::$global_data[$key]; + } else if (isset($this->$key)) { + return $this->$key; + } else { + throw new Kohana_Exception('Undefined view variable: :var', array(':var' => $key)); + } + } + + /** + * Override View_Core::__construct so that we can set the csrf value into all views. + * + * @see View_Core::__construct + */ + public function __construct($name = NULL, $data = NULL, $type = NULL) { + parent::__construct($name, $data, $type); + $this->set_global("csrf", access::csrf_token()); + } + + /** + * Override View_Core::render so that we trap errors stemming from bad PHP includes and show a + * visible stack trace to help developers. + * + * @see View_Core::render + */ + public function render($print=false, $renderer=false, $modifier=false) { + try { + $this->kohana_local_data = array_merge(View::$global_data, $this->kohana_local_data); + return parent::render($print, $renderer, $modifier); + } catch (ORM_Validation_Exception $e) { + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1)); + return ""; + } catch (Exception $e) { + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + return ""; + } + } +} diff --git a/modules/gallery/libraries/Menu.php b/modules/gallery/libraries/Menu.php new file mode 100644 index 0000000..24a05cd --- /dev/null +++ b/modules/gallery/libraries/Menu.php @@ -0,0 +1,257 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Menu_Element { + public $label; + public $url; + public $css_id; + public $css_class; + public $id; + public $type; + + public function __construct($type) { + $this->type = $type; + } + + /** + * Set the id + * @chainable + */ + public function id($id) { + $this->id = $id; + return $this; + } + + /** + * Set the label + * @chainable + */ + public function label($label) { + // Guard against developers who forget to internationalize label strings + if (!($label instanceof SafeString)) { + $label = new SafeString($label); + } + + $this->label = $label; + return $this; + } + + /** + * Set the url + * @chainable + */ + public function url($url) { + $this->url = $url; + return $this; + } + + /** + * Set the css id + * @chainable + */ + public function css_id($css_id) { + $this->css_id = $css_id; + return $this; + } + + /** + * Set the css class + * @chainable + */ + public function css_class($css_class) { + $this->css_class = $css_class; + return $this; + } + + /** + * Specifiy a view for this menu item + * @chainable + */ + public function view($view) { + $this->view = $view; + return $this; + } + +} + +/** + * Menu element that provides a link to a new page. + */ +class Menu_Element_Link extends Menu_Element { + public function render() { + $view = new View(isset($this->view) ? $this->view : "menu_link.html"); + $view->menu = $this; + return $view; + } +} + +/** + * Menu element that provides an AJAX link. + */ +class Menu_Element_Ajax_Link extends Menu_Element { + public $ajax_handler; + + /** + * Set the AJAX handler + * @chainable + */ + public function ajax_handler($ajax_handler) { + $this->ajax_handler = $ajax_handler; + return $this; + } + + public function render() { + $view = new View(isset($this->view) ? $this->view : "menu_ajax_link.html"); + $view->menu = $this; + return $view; + } +} + +/** + * Menu element that provides a pop-up dialog + */ +class Menu_Element_Dialog extends Menu_Element { + public function render() { + $view = new View(isset($this->view) ? $this->view : "menu_dialog.html"); + $view->menu = $this; + return $view; + } +} + +/** + * Root menu or submenu + */ +class Menu_Core extends Menu_Element { + public $elements; + public $is_root = false; + + /** + * Return an instance of a Menu_Element + * @chainable + */ + public static function factory($type) { + switch($type) { + case "link": + return new Menu_Element_Link($type); + + case "ajax_link": + return new Menu_Element_Ajax_Link($type); + + case "dialog": + return new Menu_Element_Dialog($type); + + case "root": + $menu = new Menu("root"); + $menu->css_class("g-menu"); + return $menu; + + case "submenu": + return new Menu("submenu"); + + default: + throw Exception("@todo UNKNOWN_MENU_TYPE"); + } + } + + public function __construct($type) { + parent::__construct($type); + $this->elements = array(); + $this->is_root = $type == "root"; + } + + /** + * Add a new element to this menu + */ + public function append($menu_element) { + $this->elements[$menu_element->id] = $menu_element; + return $this; + } + + /** + * Add a new element to this menu, after the specific element + */ + public function add_after($target_id, $new_menu_element) { + $copy = array(); + foreach ($this->elements as $id => $menu_element) { + $copy[$id] = $menu_element; + if ($id == $target_id) { + $copy[$new_menu_element->id] = $new_menu_element; + } + } + $this->elements = $copy; + return $this; + } + + /** + * Add a new element to this menu, before the specific element + */ + public function add_before($target_id, $new_menu_element) { + $copy = array(); + foreach ($this->elements as $id => $menu_element) { + if ($id == $target_id) { + $copy[$new_menu_element->id] = $new_menu_element; + } + $copy[$id] = $menu_element; + } + $this->elements = $copy; + return $this; + } + + /** + * Remove an element from the menu + */ + public function remove($target_id) { + unset($this->elements[$target_id]); + } + + /** + * Retrieve a Menu_Element by id + */ + public function &get($id) { + if (array_key_exists($id, $this->elements)) { + return $this->elements[$id]; + } + + $null = null; + return $null; + } + + public function is_empty() { + foreach ($this->elements as $element) { + if ($element instanceof Menu) { + if (!$element->is_empty()) { + return false; + } + } else { + return false; + } + } + return true; + } + + public function render() { + $view = new View(isset($this->view) ? $this->view : "menu.html"); + $view->menu = $this; + return $view; + } + + static function title_comparator($a, $b) { + return strnatcasecmp((string)$a->label, (string)$b->label); + } +} diff --git a/modules/gallery/libraries/ORM_MPTT.php b/modules/gallery/libraries/ORM_MPTT.php new file mode 100644 index 0000000..0ad8133 --- /dev/null +++ b/modules/gallery/libraries/ORM_MPTT.php @@ -0,0 +1,341 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/** + * Implement Modified Preorder Tree Traversal on top of ORM. + * + * MPTT is an efficient way to store and retrieve hierarchical data in a single database table. + * For a good description, read http://www.sitepoint.com/article/hierarchical-data-database/3/ + * + * This code was heavily influenced by code from: + * - http://code.google.com/p/kohana-mptt/ + * - http://code.google.com/p/kohana-mptt/wiki/Documentation + * - http://code.google.com/p/s7ncms/source/browse/trunk/modules/s7ncms/libraries/ORM_MPTT.php + * + * Unfortunately that code was not ready for production and I did not want to absorb their code + * and licensing issues so I've reimplemented just the features that we need. + */ +class ORM_MPTT_Core extends ORM { + private $model_name = null; + + function __construct($id=null) { + parent::__construct($id); + $this->model_name = inflector::singular($this->table_name); + } + + /** + * Overload ORM::save() to update the MPTT tree when we add new items to the hierarchy. + * + * @chainable + * @return ORM + */ + function save() { + if (!$this->loaded()) { + $this->lock(); + $parent = ORM::factory("item", $this->parent_id); + + try { + // Make a hole in the parent for this new item + db::build() + ->update($this->table_name) + ->set("left_ptr", db::expr("`left_ptr` + 2")) + ->where("left_ptr", ">=", $parent->right_ptr) + ->execute(); + db::build() + ->update($this->table_name) + ->set("right_ptr", db::expr("`right_ptr` + 2")) + ->where("right_ptr", ">=", $parent->right_ptr) + ->execute(); + $parent->right_ptr += 2; + + // Insert this item into the hole + $this->left_ptr = $parent->right_ptr - 2; + $this->right_ptr = $parent->right_ptr - 1; + $this->parent_id = $parent->id; + $this->level = $parent->level + 1; + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + parent::save(); + $this->unlock(); + } else { + parent::save(); + } + + return $this; + } + + /** + * Delete this node and all of its children. + */ + public function delete($ignored_id=null) { + $children = $this->children(); + if ($children) { + foreach ($this->children() as $item) { + // Deleting children affects the MPTT tree, so we have to reload each child before we + // delete it so that we have current left_ptr/right_ptr pointers. This is inefficient. + // @todo load each child once, not twice. + set_time_limit(30); + $item->reload()->delete(); + } + + // Deleting children has affected this item, but we'll reload it below. + } + + $this->lock(); + $this->reload(); // Assume that the prior lock holder may have changed this entry + if (!$this->loaded()) { + // Concurrent deletes may result in this item already being gone. Ignore it. + return; + } + + try { + db::build() + ->update($this->table_name) + ->set("left_ptr", db::expr("`left_ptr` - 2")) + ->where("left_ptr", ">", $this->right_ptr) + ->execute(); + db::build() + ->update($this->table_name) + ->set("right_ptr", db::expr("`right_ptr` - 2")) + ->where("right_ptr", ">", $this->right_ptr) + ->execute(); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + + $this->unlock(); + parent::delete(); + } + + /** + * Return true if the target is descendant of this item. + * @param ORM $target + * @return boolean + */ + function contains($target) { + return ($this->left_ptr <= $target->left_ptr && $this->right_ptr >= $target->right_ptr); + } + + /** + * Return the parent of this node + * + * @return ORM + */ + function parent() { + if (!$this->parent_id) { + return null; + } + return model_cache::get($this->model_name, $this->parent_id); + } + + /** + * Return all the parents of this node, in order from root to this node's immediate parent. + * + * @return array ORM + */ + function parents($where=null) { + return $this + ->merge_where($where) + ->where("left_ptr", "<=", $this->left_ptr) + ->where("right_ptr", ">=", $this->right_ptr) + ->where("id", "<>", $this->id) + ->order_by("left_ptr", "ASC") + ->find_all(); + } + + /** + * Return all of the children of this node, ordered by id. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @param array additional where clauses + * @param array order_by + * @return array ORM + */ + function children($limit=null, $offset=null, $where=null, $order_by=array("id" => "ASC")) { + return $this + ->merge_where($where) + ->where("parent_id", "=", $this->id) + ->order_by($order_by) + ->find_all($limit, $offset); + } + + /** + * Return the number of children of this node. + * + * @chainable + * @param array additional where clauses + * @return array ORM + */ + function children_count($where=null) { + return $this + ->merge_where($where) + ->where("parent_id", "=", $this->id) + ->count_all(); + } + + /** + * Return all of the decendents of the specified type, ordered by id. + * + * @param integer SQL limit + * @param integer SQL offset + * @param array additional where clauses + * @param array order_by + * @return object ORM_Iterator + */ + function descendants($limit=null, $offset=null, $where=null, $order_by=array("id" => "ASC")) { + return $this + ->merge_where($where) + ->where("left_ptr", ">", $this->left_ptr) + ->where("right_ptr", "<=", $this->right_ptr) + ->order_by($order_by) + ->find_all($limit, $offset); + } + + /** + * Return the count of all the children of the specified type. + * + * @param array additional where clauses + * @return integer child count + */ + function descendants_count($where=null) { + return $this + ->merge_where($where) + ->where("left_ptr", ">", $this->left_ptr) + ->where("right_ptr", "<=", $this->right_ptr) + ->count_all(); + } + + /** + * Move this item to the specified target. + * + * @chainable + * @param Item_Model $target Target node + * @return ORM_MTPP + */ + protected function move_to($target) { + if ($this->contains($target)) { + throw new Exception("@todo INVALID_TARGET can't move item inside itself"); + } + + $this->lock(); + $this->reload(); // Assume that the prior lock holder may have changed this entry + $target->reload(); + + $number_to_move = (int)(($this->right_ptr - $this->left_ptr) / 2 + 1); + $size_of_hole = $number_to_move * 2; + $original_left_ptr = $this->left_ptr; + $original_right_ptr = $this->right_ptr; + $target_right_ptr = $target->right_ptr; + $level_delta = ($target->level + 1) - $this->level; + + try { + if ($level_delta) { + // Update the levels for the to-be-moved items + db::build() + ->update($this->table_name) + ->set("level", db::expr("`level` + $level_delta")) + ->where("left_ptr", ">=", $original_left_ptr) + ->where("right_ptr", "<=", $original_right_ptr) + ->execute(); + } + + // Make a hole in the target for the move + db::build() + ->update($this->table_name) + ->set("left_ptr", db::expr("`left_ptr` + $size_of_hole")) + ->where("left_ptr", ">=", $target_right_ptr) + ->execute(); + db::build() + ->update($this->table_name) + ->set("right_ptr", db::expr("`right_ptr` + $size_of_hole")) + ->where("right_ptr", ">=", $target_right_ptr) + ->execute(); + + // Change the parent. + db::build() + ->update($this->table_name) + ->set("parent_id", $target->id) + ->where("id", "=", $this->id) + ->execute(); + + // If the source is to the right of the target then we just adjusted its left_ptr and + // right_ptr above. + $left_ptr = $original_left_ptr; + $right_ptr = $original_right_ptr; + if ($original_left_ptr > $target_right_ptr) { + $left_ptr += $size_of_hole; + $right_ptr += $size_of_hole; + } + + $new_offset = $target->right_ptr - $left_ptr; + db::build() + ->update($this->table_name) + ->set("left_ptr", db::expr("`left_ptr` + $new_offset")) + ->set("right_ptr", db::expr("`right_ptr` + $new_offset")) + ->where("left_ptr", ">=", $left_ptr) + ->where("right_ptr", "<=", $right_ptr) + ->execute(); + + // Close the hole in the source's parent after the move + db::build() + ->update($this->table_name) + ->set("left_ptr", db::expr("`left_ptr` - $size_of_hole")) + ->where("left_ptr", ">", $right_ptr) + ->execute(); + db::build() + ->update($this->table_name) + ->set("right_ptr", db::expr("`right_ptr` - $size_of_hole")) + ->where("right_ptr", ">", $right_ptr) + ->execute(); + } catch (Exception $e) { + $this->unlock(); + throw $e; + } + + $this->unlock(); + + // Lets reload to get the changes. + $this->reload(); + $target->reload(); + return $this; + } + + /** + * Lock the tree to prevent concurrent modification. + */ + protected function lock() { + $timeout = module::get_var("gallery", "lock_timeout", 1); + $result = $this->db->query("SELECT GET_LOCK('{$this->table_name}', $timeout) AS l")->current(); + if (empty($result->l)) { + throw new Exception("@todo UNABLE_TO_LOCK_EXCEPTION"); + } + } + + /** + * Unlock the tree. + */ + protected function unlock() { + $this->db->query("SELECT RELEASE_LOCK('{$this->table_name}')"); + } +} diff --git a/modules/gallery/libraries/SafeString.php b/modules/gallery/libraries/SafeString.php new file mode 100644 index 0000000..179cbd4 --- /dev/null +++ b/modules/gallery/libraries/SafeString.php @@ -0,0 +1,162 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * Safe string representation (regarding security - cross site scripting). + */ +class SafeString_Core { + private $_raw_string; + protected $_is_safe_html = false; + + /** Constructor */ + function __construct($string) { + if ($string instanceof SafeString) { + $this->_is_safe_html = $string->_is_safe_html; + $string = $string->unescaped(); + } + $this->_raw_string = (string) $string; + } + + /** + * Factory method returning a new SafeString instance for the given string. + */ + static function of($string) { + return new SafeString($string); + } + + /** + * Factory method returning a new SafeString instance after HTML purifying + * the given string. + */ + static function purify($string) { + if ($string instanceof SafeString) { + if ($string->_is_safe_html) { + return $string; + } else { + $string = $string->unescaped(); + } + } + $safe_string = self::of_safe_html(self::_purify_for_html($string)); + return $safe_string; + } + + /** + * Factory method returning a new SafeString instance which won't HTML escape. + */ + static function of_safe_html($string) { + $safe_string = new SafeString($string); + $safe_string->_is_safe_html = true; + return $safe_string; + } + + /** + * Safe for use in HTML. + * @see #for_html() + */ + function __toString() { + if ($this->_is_safe_html) { + return $this->_raw_string; + } else { + return self::_escape_for_html($this->_raw_string); + } + } + + /** + * Safe for use in HTML. + * + * Example:<pre> + * <div><?= $php_var ?> + * </pre> + * @return the string escaped for use in HTML. + */ + function for_html() { + return $this; + } + + /** + * Safe for use as JavaScript string. + * + * Example:<pre> + * <script type="text/javascript>" + * var some_js_var = <?= $php_var->for_js() ?>; + * </script> + * </pre> + * @return the string escaped for use in JavaScript. + */ + function for_js() { + return json_encode((string) $this->_raw_string); + } + + /** + * Safe for use in HTML element attributes. + * + * Assumes that the HTML element attribute is already + * delimited by single or double quotes + * + * Example:<pre> + * <a title="<?= $php_var->for_html_attr() ?>">; + * </script> + * </pre> + * @return the string escaped for use in HTML attributes. + */ + function for_html_attr() { + $string = (string) $this->for_html(); + return strtr($string, + array("'"=>"'", + '"'=>'"')); + } + + /** + * Safe for use HTML (purified HTML) + * + * Example:<pre> + * <div><?= $php_var->purified_html() ?> + * </pre> + * @return the string escaped for use in HTML. + */ + function purified_html() { + return self::purify($this); + } + + /** + * Returns the raw, unsafe string. Do not use lightly. + */ + function unescaped() { + return $this->_raw_string; + } + + /** + * Escape special HTML chars ("<", ">", "&", etc.) to HTML entities. + */ + private static function _escape_for_html($dirty_html) { + return html::chars($dirty_html); + } + + /** + * Purify the string, removing any potentially malicious or unsafe HTML / JavaScript. + */ + private static function _purify_for_html($dirty_html) { + if (class_exists("purifier") && method_exists("purifier", "purify")) { + return purifier::purify($dirty_html); + } else { + return self::_escape_for_html($dirty_html); + } + } +} diff --git a/modules/gallery/libraries/Sendmail.php b/modules/gallery/libraries/Sendmail.php new file mode 100644 index 0000000..69a7c32 --- /dev/null +++ b/modules/gallery/libraries/Sendmail.php @@ -0,0 +1,98 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Sendmail_Core { + protected $to; + protected $subject; + protected $message; + protected $headers; + protected $line_length = 70; + protected $header_separator = "\r\n"; + + /** + * Return an instance of Sendmail + * @chainable + */ + static function factory() { + return new Sendmail(); + } + + public function __construct() { + $this->headers = array(); + $this->from(module::get_var("gallery", "email_from", "")); + $this->reply_to(module::get_var("gallery", "email_reply_to", "")); + $this->line_length(module::get_var("gallery", "email_line_length", 70)); + $separator = module::get_var("gallery", "email_header_separator", null); + $this->header_separator(empty($separator) ? "\n" : unserialize($separator)); + } + + public function __get($key) { + return null; + } + + public function __call($key, $value) { + switch ($key) { + case "to": + $this->to = is_array($value[0]) ? $value[0] : array($value[0]); + break; + case "header": + if (count($value) != 2) { + Kohana_Log::add("error", wordwrap("Invalid header parameters\n" . Kohana::debug($value))); + throw new Exception("@todo INVALID_HEADER_PARAMETERS"); + } + $this->headers[$value[0]] = $value[1]; + break; + case "from": + $this->headers["From"] = $value[0]; + break; + case "reply_to": + $this->headers["Reply-To"] = $value[0]; + break; + default: + $this->$key = $value[0]; + } + return $this; + } + + public function send() { + if (empty($this->to)) { + Kohana_Log::add("error", wordwrap("Sending mail failed:\nNo to address specified")); + throw new Exception("@todo TO_IS_REQUIRED_FOR_MAIL"); + } + $to = implode(", ", $this->to); + $headers = array(); + foreach ($this->headers as $key => $value) { + $key = ucfirst($key); + $headers[] = "$key: $value"; + } + + // The docs say headers should be separated by \r\n, but occasionaly that doesn't work and you + // need to use a single \n. This can be set in config/sendmail.php + $headers = implode($this->header_separator, $headers); + $message = wordwrap($this->message, $this->line_length, "\n"); + if (!$this->mail($to, $this->subject, $message, $headers)) { + throw new Exception("@todo SEND_MAIL_FAILED"); + } + return $this; + } + + public function mail($to, $subject, $message, $headers) { + return mail($to, $subject, $message, $headers); + } +} diff --git a/modules/gallery/libraries/Task_Definition.php b/modules/gallery/libraries/Task_Definition.php new file mode 100644 index 0000000..f695fe3 --- /dev/null +++ b/modules/gallery/libraries/Task_Definition.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class Task_Definition_Core { + public $callback; + public $description; + public $name; + public $severity; + + static function factory() { + return new Task_Definition(); + } + + function callback($callback) { + $this->callback = $callback; + return $this; + } + + function description($description) { + $this->description = $description; + return $this; + } + + function name($name) { + $this->name = $name; + return $this; + } + + function severity($severity) { + $this->severity = $severity; + return $this; + } +} diff --git a/modules/gallery/libraries/Theme_View.php b/modules/gallery/libraries/Theme_View.php new file mode 100644 index 0000000..9118375 --- /dev/null +++ b/modules/gallery/libraries/Theme_View.php @@ -0,0 +1,271 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Theme_View_Core extends Gallery_View { + /** + * Attempts to load a view and pre-load view data. + * + * @throws Kohana_Exception if the requested view cannot be found + * @param string $name view name + * @param string $page_type page type: collection, item, or other + * @param string $page_subtype page sub type: album, photo, tags, etc + * @param string $theme_name view name + * @return void + */ + public function __construct($name, $page_type, $page_subtype) { + parent::__construct($name); + + $this->theme_name = module::get_var("gallery", "active_site_theme"); + if (identity::active_user()->admin) { + $theme_name = Input::instance()->get("theme"); + if ($theme_name && + file_exists(THEMEPATH . $theme_name) && + strpos(realpath(THEMEPATH . $theme_name), THEMEPATH) == 0) { + $this->theme_name = $theme_name; + } + } + $this->item = null; + $this->tag = null; + $this->set_global(array("theme" => $this, + "theme_info" => theme::get_info($this->theme_name), + "user" => identity::active_user(), + "page_type" => $page_type, + "page_subtype" => $page_subtype, + "page_title" => null)); + + if (module::get_var("gallery", "maintenance_mode", 0)) { + if (identity::active_user()->admin) { + message::warning(t("This site is currently in maintenance mode. Visit the <a href=\"%maintenance_url\">maintenance page</a>", array("maintenance_url" => url::site("admin/maintenance")))); + } else + message::warning(t("This site is currently in maintenance mode.")); + } + } + + /** + * Proportion of the current thumb_size's to default + * @param object Item_Model (optional) check the proportions for this item + * @return int + */ + public function thumb_proportion($item=null) { + // If the item is an album with children, grab the first item in that album instead. We're + // interested in the size of the thumbnails in this album, not the thumbnail of the + // album itself. + if ($item && $item->is_album() && $item->children_count()) { + $item = $item->children(1)->current(); + } + + // By default we have a globally fixed thumbnail size In core code, we just return a fixed + // proportion based on the global thumbnail size, but since modules can override that, we + // return the actual proportions when we have them. + if ($item && $item->has_thumb()) { + return max($item->thumb_width, $item->thumb_height) / 200; + } else { + // @TODO change the 200 to a theme supplied value when and if we come up with an + // API to allow the theme to set defaults. + return module::get_var("gallery", "thumb_size", 200) / 200; + } + } + + public function item() { + return $this->item; + } + + public function siblings($limit=null, $offset=null) { + return call_user_func_array( + $this->siblings_callback[0], + array_merge($this->siblings_callback[1], array($limit, $offset))); + } + + public function tag() { + return $this->tag; + } + + public function page_type() { + return $this->page_type; + } + + public function page_subtype() { + return $this->page_subtype; + } + + public function user_menu() { + $menu = Menu::factory("root") + ->css_id("g-login-menu") + ->css_class("g-inline ui-helper-clear-fix"); + module::event("user_menu", $menu, $this); + return $menu->render(); + } + + public function site_menu($item_css_selector) { + $menu = Menu::factory("root"); + module::event("site_menu", $menu, $this, $item_css_selector); + return $menu->render(); + } + + public function album_menu() { + $menu = Menu::factory("root"); + module::event("album_menu", $menu, $this); + return $menu->render(); + } + + public function tag_menu() { + $menu = Menu::factory("root"); + module::event("tag_menu", $menu, $this); + return $menu->render(); + } + + public function photo_menu() { + $menu = Menu::factory("root"); + if (access::can("view_full", $this->item())) { + $menu->append(Menu::factory("link") + ->id("fullsize") + ->label(t("View full size")) + ->url($this->item()->file_url()) + ->css_class("g-fullsize-link")); + } + + module::event("photo_menu", $menu, $this); + return $menu->render(); + } + + public function movie_menu() { + $menu = Menu::factory("root"); + module::event("movie_menu", $menu, $this); + return $menu->render(); + } + + public function context_menu($item, $thumbnail_css_selector) { + $menu = Menu::factory("root") + ->append(Menu::factory("submenu") + ->id("context_menu") + ->label(t("Options"))) + ->css_class("g-context-menu"); + + module::event("context_menu", $menu, $this, $item, $thumbnail_css_selector); + return $menu->render(); + } + + /** + * Print out any site wide status information. + */ + public function site_status() { + return site_status::get(); + } + + /** + * Print out any messages waiting for this user. + */ + public function messages() { + return message::get(); + } + + /** + * Print out the sidebar. + */ + public function sidebar_blocks() { + $sidebar = block_manager::get_html("site_sidebar", $this); + if (empty($sidebar) && identity::active_user()->admin) { + $sidebar = new View("no_sidebar.html"); + } + return $sidebar; + } + + /** + * Handle all theme functions that insert module content. + */ + public function __call($function, $args) { + switch ($function) { + case "album_blocks": + case "album_bottom": + case "album_top": + case "body_attributes": + case "credits"; + case "dynamic_bottom": + case "dynamic_top": + case "footer": + case "head": + case "header_bottom": + case "header_top": + case "html_attributes": + case "page_bottom": + case "page_top": + case "photo_blocks": + case "photo_bottom": + case "photo_top": + case "resize_bottom": + case "resize_top": + case "sidebar_bottom": + case "sidebar_top": + case "thumb_bottom": + case "thumb_info": + case "thumb_top": + $blocks = array(); + if (method_exists("gallery_theme", $function)) { + switch (count($args)) { + case 0: + $blocks[] = gallery_theme::$function($this); + break; + case 1: + $blocks[] = gallery_theme::$function($this, $args[0]); + break; + case 2: + $blocks[] = gallery_theme::$function($this, $args[0], $args[1]); + break; + default: + $blocks[] = call_user_func_array( + array("gallery_theme", $function), + array_merge(array($this), $args)); + } + } + + foreach (module::active() as $module) { + if ($module->name == "gallery") { + continue; + } + $helper_class = "{$module->name}_theme"; + if (class_exists($helper_class) && method_exists($helper_class, $function)) { + $blocks[] = call_user_func_array( + array($helper_class, $function), + array_merge(array($this), $args)); + } + } + + $helper_class = theme::$site_theme_name . "_theme"; + if (class_exists($helper_class) && method_exists($helper_class, $function)) { + $blocks[] = call_user_func_array( + array($helper_class, $function), + array_merge(array($this), $args)); + } + + if (Session::instance()->get("debug")) { + if ($function != "head" && $function != "body_attributes") { + array_unshift( + $blocks, + "<div class=\"g-annotated-theme-block g-annotated-theme-block_$function g-clear-fix\">" . + "<div class=\"title\">$function</div>"); + $blocks[] = "</div>"; + } + } + return implode("\n", $blocks); + + default: + throw new Exception("@todo UNKNOWN_THEME_FUNCTION: $function"); + } + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/drivers/Cache/Database.php b/modules/gallery/libraries/drivers/Cache/Database.php new file mode 100644 index 0000000..8790d0e --- /dev/null +++ b/modules/gallery/libraries/drivers/Cache/Database.php @@ -0,0 +1,166 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* + * Based on the Cache_Sqlite_Driver developed by the Kohana Team + */ +class Cache_Database_Driver extends Cache_Driver { + // Kohana database instance + protected $db; + + /** + * Sets a cache item to the given data, tags, and lifetime. + * + * @param array assoc array of key => value pairs + * @param array cache tags + * @param integer lifetime + * @return bool + */ + public function set($items, $tags=null, $lifetime=null) { + if (!empty($tags)) { + // Escape the tags, adding brackets so the tag can be explicitly matched + $tags = "<" . implode(">,<", $tags) . ">"; + } else { + $tags = null; + } + + // Cache Database driver expects unix timestamp + if ($lifetime !== 0) { + $lifetime += time(); + } + + $db = Database::instance(); + $tags = $db->escape($tags); + foreach ($items as $id => $data) { + $id = $db->escape($id); + $data = $db->escape(serialize($data)); + $db->query("INSERT INTO {caches} (`key`, `tags`, `expiration`, `cache`) + VALUES ('$id', '$tags', $lifetime, '$data') + ON DUPLICATE KEY UPDATE `tags` = VALUES(tags), `expiration` = VALUES(expiration), + `cache` = VALUES(cache)"); + } + + return true; + } + + /** + * Get cache items by tag + * @param array cache tags + * @return array cached data + */ + public function get_tag($tags) { + $db = db::build() + ->select() + ->from("caches"); + foreach ($tags as $tag) { + $db->where("tags", "LIKE", "%" . Database::escape_for_like("<$tag>") . "%"); + } + $db_result = $db->execute(); + + // An array will always be returned + $result = array(); + + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + if ($db_result->count() > 0) { + foreach ($db_result as $row) { + // Add each cache to the array + $result[$row->key] = unserialize($row->cache); + } + } + error_reporting($ER); + + return $result; + } + + /** + * Fetches a cache item. This will delete the item if it is expired or if + * the hash does not match the stored hash. + * + * @param string cache id + * @return mixed|NULL + */ + public function get($keys, $single=false) { + $data = null; + $result = db::build() + ->select() + ->from("caches") + ->where("key", "IN", $keys) + ->execute(); + + if (count($result) > 0) { + $cache = $result->current(); + // Make sure the expiration is valid and that the hash matches + if ($cache->expiration != 0 && $cache->expiration <= time()) { + // Cache is not valid, delete it now + $this->delete(array($cache->id)); + } else { + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + + // Return the valid cache data + $data = unserialize($cache->cache); + + // Turn notices back on + error_reporting($ER); + } + } + + return $data; + } + + /** + * Deletes a cache item by id or tag + * + * @param string cache id or tag, or true for "all items" + * @param bool delete a tag + * @return bool + */ + public function delete($keys, $is_tag=false) { + $db = db::build() + ->delete("caches"); + if ($keys === true) { + // Delete all caches + } else if ($is_tag === true) { + foreach ($keys as $tag) { + $db->where("tags", "LIKE", "%" . Database::escape_for_like("<$tag>") . "%"); + } + } else { + $db->where("key", "IN", $keys); + } + + $status = $db->execute(); + + return count($status) > 0; + } + + /** + * Delete cache items by tag + */ + public function delete_tag($tags) { + return $this->delete($tags, true); + } + + /** + * Empty the cache + */ + public function delete_all() { + Database::instance()->query("TRUNCATE {caches}"); + } +}
\ No newline at end of file diff --git a/modules/gallery/libraries/drivers/IdentityProvider.php b/modules/gallery/libraries/drivers/IdentityProvider.php new file mode 100644 index 0000000..5256236 --- /dev/null +++ b/modules/gallery/libraries/drivers/IdentityProvider.php @@ -0,0 +1,134 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +interface IdentityProvider_Driver { + /** + * Return the guest user. + * + * @return User_Definition the user object + */ + public function guest(); + + /** + * Return the primary admin user. + * + * @return User_Definition the user object + */ + public function admin_user(); + + /** + * Create a new user. + * + * @param string $name + * @param string $full_name + * @param string $password + * @param string $email + * @return User_Definition the user object + */ + public function create_user($name, $full_name, $password, $email); + + /** + * Is the password provided correct? + * + * @param user User_Definition the user object + * @param string $password a plaintext password + * @return boolean true if the password is correct + */ + public function is_correct_password($user, $password); + + /** + * Look up a user by id. + * @param integer $id + * @return User_Definition the user object, or null if the name was invalid. + */ + public function lookup_user($id); + + /** + * Look up a user by name. + * @param string $name + * @return User_Definition the user object, or null if the name was invalid. + */ + public function lookup_user_by_name($name); + + /** + * Create a new group. + * + * @param string $name + * @return Group_Definition the group object + */ + public function create_group($name); + + /** + * The group of all possible visitors. This includes the guest user. + * + * @return Group_Definition the group object + */ + public function everybody(); + + /** + * The group of all logged-in visitors. This does not include guest users. + * + * @return Group_Definition the group object + */ + public function registered_users(); + + /** + * List the users + * @param array $ids array of ids to return the user objects for + * @return array the user list. + */ + public function get_user_list($ids); + + /** + * Look up a group by id. + * @param integer $id id + * @return Group_Definition the user object, or null if the name was invalid. + */ + public function lookup_group($id); + + /** + * Look up the group by name. + * @param string $name the name of the group to locate + * @return Group_Definition + */ + public function lookup_group_by_name($name); + + /** + * List the groups defined in the Identity Provider + */ + public function groups(); + + /** + * Add the user to the specified group + * @param User_Definition the user to add + * @param Group_Definition the target group + */ + public function add_user_to_group($user, $group); + + /** + * Remove the user to the specified group + * @param User_Definition the user to remove + * @param Group_Definition the owning group + */ + public function remove_user_from_group($user, $group); +} // End Identity Driver Definition + +interface Group_Definition {} + +interface User_Definition {} diff --git a/modules/gallery/models/access_cache.php b/modules/gallery/models/access_cache.php new file mode 100644 index 0000000..b0c0843 --- /dev/null +++ b/modules/gallery/models/access_cache.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Access_Cache_Model_Core extends ORM { +} diff --git a/modules/gallery/models/access_intent.php b/modules/gallery/models/access_intent.php new file mode 100644 index 0000000..56f9666 --- /dev/null +++ b/modules/gallery/models/access_intent.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Access_Intent_Model_Core extends ORM { +} diff --git a/modules/gallery/models/cache.php b/modules/gallery/models/cache.php new file mode 100644 index 0000000..37d5265 --- /dev/null +++ b/modules/gallery/models/cache.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Cache_Model_Core extends ORM {} diff --git a/modules/gallery/models/failed_auth.php b/modules/gallery/models/failed_auth.php new file mode 100644 index 0000000..85c6492 --- /dev/null +++ b/modules/gallery/models/failed_auth.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Failed_Auth_Model_Core extends ORM {} diff --git a/modules/gallery/models/graphics_rule.php b/modules/gallery/models/graphics_rule.php new file mode 100644 index 0000000..241560e --- /dev/null +++ b/modules/gallery/models/graphics_rule.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Graphics_Rule_Model_Core extends ORM { +} diff --git a/modules/gallery/models/incoming_translation.php b/modules/gallery/models/incoming_translation.php new file mode 100644 index 0000000..f3c9813 --- /dev/null +++ b/modules/gallery/models/incoming_translation.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Incoming_Translation_Model_Core extends ORM { +} diff --git a/modules/gallery/models/item.php b/modules/gallery/models/item.php new file mode 100644 index 0000000..c446eea --- /dev/null +++ b/modules/gallery/models/item.php @@ -0,0 +1,1191 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Item_Model_Core extends ORM_MPTT { + protected $children = "items"; + protected $sorting = array(); + public $data_file = null; + private $data_file_error = null; + + public function __construct($id=null) { + parent::__construct($id); + + if (!$this->loaded()) { + // Set reasonable defaults + $this->created = time(); + $this->rand_key = random::percent(); + $this->thumb_dirty = 1; + $this->resize_dirty = 1; + $this->sort_column = "created"; + $this->sort_order = "ASC"; + $this->owner_id = identity::active_user()->id; + } + } + + /** + * Add a set of restrictions to any following queries to restrict access only to items + * viewable by the active user. + * @chainable + */ + public function viewable() { + return item::viewable($this); + } + + /** + * Is this item an album? + * @return true if it's an album + */ + public function is_album() { + return $this->type == 'album'; + } + + /** + * Is this item a photo? + * @return true if it's a photo + */ + public function is_photo() { + return $this->type == 'photo'; + } + + /** + * Is this item a movie? + * @return true if it's a movie + */ + public function is_movie() { + return $this->type == 'movie'; + } + + public function delete($ignored_id=null) { + if (!$this->loaded()) { + // Concurrent deletes may result in this item already being gone. Ignore it. + return; + } + + if ($this->id == 1) { + $v = new Validation(array("id")); + $v->add_error("id", "cant_delete_root_album"); + ORM_Validation_Exception::handle_validation($this->table_name, $v); + } + + $old = clone $this; + module::event("item_before_delete", $this); + + $parent = $this->parent(); + if ($parent->album_cover_item_id == $this->id) { + item::remove_album_cover($parent); + } + + $path = $this->file_path(); + $resize_path = $this->resize_path(); + $thumb_path = $this->thumb_path(); + + parent::delete(); + if (is_dir($path)) { + // Take some precautions against accidentally deleting way too much + $delete_resize_path = dirname($resize_path); + $delete_thumb_path = dirname($thumb_path); + if ($delete_resize_path == VARPATH . "resizes" || + $delete_thumb_path == VARPATH . "thumbs" || + $path == VARPATH . "albums") { + throw new Exception( + "@todo DELETING_TOO_MUCH ($delete_resize_path, $delete_thumb_path, $path)"); + } + @dir::unlink($path); + @dir::unlink($delete_resize_path); + @dir::unlink($delete_thumb_path); + } else { + @unlink($path); + @unlink($resize_path); + @unlink($thumb_path); + } + + module::event("item_deleted", $old); + } + + /** + * Specify the path to the data file associated with this item. To actually associate it, + * you still have to call save(). + * @chainable + */ + public function set_data_file($data_file) { + $this->data_file = $data_file; + return $this; + } + + /** + * Return the server-relative url to this item, eg: + * album: /gallery3/index.php/Bobs%20Wedding?page=2 + * photo: /gallery3/index.php/Bobs%20Wedding/Eating-Cake + * movie: /gallery3/index.php/Bobs%20Wedding/First-Dance + * + * @param string $query the query string (eg "page=2") + */ + public function url($query=null) { + $url = url::site($this->relative_url()); + if ($query) { + $url .= "?$query"; + } + return $url; + } + + /** + * Return the full url to this item, eg: + * album: http://example.com/gallery3/index.php/Bobs%20Wedding?page=2 + * photo: http://example.com/gallery3/index.php/Bobs%20Wedding/Eating-Cake + * movie: http://example.com/gallery3/index.php/Bobs%20Wedding/First-Dance + * + * @param string $query the query string (eg "page=2") + */ + public function abs_url($query=null) { + $url = url::abs_site($this->relative_url()); + if ($query) { + $url .= "?$query"; + } + return $url; + } + + /** + * Return the full path to this item's file, eg: + * album: /usr/home/www/gallery3/var/albums/Bobs Wedding + * photo: /usr/home/www/gallery3/var/albums/Bobs Wedding/Eating-Cake.jpg + * movie: /usr/home/www/gallery3/var/albums/Bobs Wedding/First-Dance.mp4 + */ + public function file_path() { + return VARPATH . "albums/" . urldecode($this->relative_path()); + } + + /** + * Return the relative url to this item's file, with cache buster, eg: + * album: var/albums/Bobs%20Wedding?m=1234567890 + * photo: var/albums/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: var/albums/Bobs%20Wedding/First-Dance.mp4?m=1234567890 + * If $full_uri==true, return the full url to this item's file, with cache buster, eg: + * album: http://example.com/gallery3/var/albums/Bobs%20Wedding?m=1234567890 + * photo: http://example.com/gallery3/var/albums/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: http://example.com/gallery3/var/albums/Bobs%20Wedding/First-Dance.mp4?m=1234567890 + */ + public function file_url($full_uri=false) { + $relative_path = "var/albums/" . $this->relative_path(); + $cache_buster = $this->_cache_buster($this->file_path()); + return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path)) + . $cache_buster; + } + + /** + * Return the full path to this item's thumb, eg: + * album: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/.album.jpg + * photo: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/Eating-Cake.jpg + * movie: /usr/home/www/gallery3/var/thumbs/Bobs Wedding/First-Dance.jpg + */ + public function thumb_path() { + $base = VARPATH . "thumbs/" . urldecode($this->relative_path()); + if ($this->is_photo()) { + return $base; + } else if ($this->is_album()) { + return $base . "/.album.jpg"; + } else if ($this->is_movie()) { + // Replace the extension with jpg + return legal_file::change_extension($base, "jpg"); + } + } + + /** + * Return true if there is a thumbnail for this item. + */ + public function has_thumb() { + return $this->thumb_width && $this->thumb_height; + } + + /** + * Return the relative url to this item's thumb, with cache buster, eg: + * album: var/thumbs/Bobs%20Wedding/.album.jpg?m=1234567890 + * photo: var/thumbs/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: var/thumbs/Bobs%20Wedding/First-Dance.mp4?m=1234567890 + * If $full_uri==true, return the full url to this item's file, with cache buster, eg: + * album: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/.album.jpg?m=1234567890 + * photo: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: http://example.com/gallery3/var/thumbs/Bobs%20Wedding/First-Dance.mp4?m=1234567890 + */ + public function thumb_url($full_uri=false) { + $cache_buster = $this->_cache_buster($this->thumb_path()); + $relative_path = "var/thumbs/" . $this->relative_path(); + $base = ($full_uri ? url::abs_file($relative_path) : url::file($relative_path)); + if ($this->is_photo()) { + return $base . $cache_buster; + } else if ($this->is_album()) { + return $base . "/.album.jpg" . $cache_buster; + } else if ($this->is_movie()) { + // Replace the extension with jpg + $base = legal_file::change_extension($base, "jpg"); + return $base . $cache_buster; + } + } + + /** + * Return the full path to this item's resize, eg: + * album: /usr/home/www/gallery3/var/resizes/Bobs Wedding/.album.jpg (*) + * photo: /usr/home/www/gallery3/var/resizes/Bobs Wedding/Eating-Cake.jpg + * movie: /usr/home/www/gallery3/var/resizes/Bobs Wedding/First-Dance.mp4 (*) + * (*) Since only photos have resizes, album and movie paths are fictitious. + */ + public function resize_path() { + return VARPATH . "resizes/" . urldecode($this->relative_path()) . + ($this->is_album() ? "/.album.jpg" : ""); + } + + /** + * Return the relative url to this item's resize, with cache buster, eg: + * album: var/resizes/Bobs%20Wedding/.album.jpg?m=1234567890 (*) + * photo: var/resizes/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: var/resizes/Bobs%20Wedding/First-Dance.mp4?m=1234567890 (*) + * If $full_uri==true, return the full url to this item's file, with cache buster, eg: + * album: http://example.com/gallery3/var/resizes/Bobs%20Wedding/.album.jpg?m=1234567890 (*) + * photo: http://example.com/gallery3/var/resizes/Bobs%20Wedding/Eating-Cake.jpg?m=1234567890 + * movie: http://example.com/gallery3/var/resizes/Bobs%20Wedding/First-Dance.mp4?m=1234567890 (*) + * (*) Since only photos have resizes, album and movie urls are fictitious. + */ + public function resize_url($full_uri=false) { + $relative_path = "var/resizes/" . $this->relative_path(); + $cache_buster = $this->_cache_buster($this->resize_path()); + return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path)) . + ($this->is_album() ? "/.album.jpg" : "") . $cache_buster; + } + + /** + * Rebuild the relative_path_cache and relative_url_cache. + */ + private function _build_relative_caches() { + $names = array(); + $slugs = array(); + foreach (db::build() + ->select(array("name", "slug")) + ->from("items") + ->where("left_ptr", "<=", $this->left_ptr) + ->where("right_ptr", ">=", $this->right_ptr) + ->where("id", "<>", 1) + ->order_by("left_ptr", "ASC") + ->execute() as $row) { + // Don't encode the names segment + $names[] = rawurlencode($row->name); + $slugs[] = rawurlencode($row->slug); + } + $this->relative_path_cache = implode($names, "/"); + $this->relative_url_cache = implode($slugs, "/"); + return $this; + } + + /** + * Return the relative path to this item's file. Note that the components of the path are + * urlencoded so if you want to use this as a filesystem path, you need to call urldecode + * on it. + * @return string + */ + public function relative_path() { + if (!$this->loaded()) { + return; + } + + if (!isset($this->relative_path_cache)) { + $this->_build_relative_caches()->save(); + } + return $this->relative_path_cache; + } + + /** + * Return the relative url to this item's file. + * @return string + */ + public function relative_url() { + if (!$this->loaded()) { + return; + } + + if (!isset($this->relative_url_cache)) { + $this->_build_relative_caches()->save(); + } + return $this->relative_url_cache; + } + + /** + * @see ORM::__get() + */ + public function __get($column) { + if ($column == "owner") { + // This relationship depends on an outside module, which may not be present so handle + // failures gracefully. + try { + return identity::lookup_user($this->owner_id); + } catch (Exception $e) { + return null; + } + } else { + return parent::__get($column); + } + } + + /** + * Handle any business logic necessary to create or modify an item. + * @see ORM::save() + * + * @return ORM Item_Model + */ + public function save() { + $significant_changes = $this->changed; + foreach (array("view_count", "relative_url_cache", "relative_path_cache", + "resize_width", "resize_height", "resize_dirty", + "thumb_width", "thumb_height", "thumb_dirty") as $key) { + unset($significant_changes[$key]); + } + + if ((!empty($this->changed) && $significant_changes) || isset($this->data_file)) { + $this->updated = time(); + if (!$this->loaded()) { + // Create a new item. + module::event("item_before_create", $this); + + // Set a weight if it's missing. We don't do this in the constructor because it's not a + // simple assignment. + if (empty($this->weight)) { + $this->weight = item::get_max_weight(); + } + + // Process the data file info. + if (isset($this->data_file)) { + $this->_process_data_file_info(); + } else if (!$this->is_album()) { + // Unless it's an album, new items must have a data file. + $this->data_file_error = true; + } + + // Make an url friendly slug from the name, if necessary + if (empty($this->slug)) { + $this->slug = item::convert_filename_to_slug(pathinfo($this->name, PATHINFO_FILENAME)); + + // If the filename is all invalid characters, then the slug may be empty here. We set a + // generic name ("photo", "movie", or "album") based on its type, then rely on + // check_and_fix_conflicts to ensure it doesn't conflict with another name. + if (empty($this->slug)) { + $this->slug = $this->type; + } + } + + $this->_check_and_fix_conflicts(); + + parent::save(); + + // Build our url caches, then save again. We have to do this after it's already been + // saved once because we use only information from the database to build the paths. If we + // could depend on a save happening later we could defer this 2nd save. + $this->_build_relative_caches(); + parent::save(); + + // Take any actions that we can only do once all our paths are set correctly after saving. + switch ($this->type) { + case "album": + mkdir($this->file_path()); + mkdir(dirname($this->thumb_path())); + mkdir(dirname($this->resize_path())); + break; + + case "photo": + case "movie": + copy($this->data_file, $this->file_path()); + break; + } + + // This will almost definitely trigger another save, so put it at the end so that we're + // tail recursive. Null out the data file variable first, otherwise the next save will + // trigger an item_updated_data_file event. + $this->data_file = null; + module::event("item_created", $this); + } else { + // Update an existing item + module::event("item_before_update", $this); + + // If any significant fields have changed, load up a copy of the original item and + // keep it around. + $original = ORM::factory("item", $this->id); + + // If we have a new data file, process its info. This will get its metadata and + // preserve the extension of the data file. Many helpers, (e.g. ImageMagick), assume + // the MIME type from the extension. So when we adopt the new data file, it's important + // to adopt the new extension. That ensures that the item's extension is always + // appropriate for its data. We don't try to preserve the name of the data file, though, + // because the name is typically a temporary randomly-generated name. + if (isset($this->data_file)) { + $this->_process_data_file_info(); + } else if (!$this->is_album() && array_key_exists("name", $this->changed)) { + // There's no new data file, but the name changed. If it's a photo or movie, + // make sure the new name still agrees with the file type. + $this->name = legal_file::sanitize_filename($this->name, + pathinfo($original->name, PATHINFO_EXTENSION), $this->type); + } + + // If an album's cover has changed (or been removed), delete any existing album cover, + // reset the thumb metadata, and mark the thumb as dirty. + if (array_key_exists("album_cover_item_id", $this->changed) && $this->is_album()) { + @unlink($original->thumb_path()); + $this->thumb_dirty = 1; + $this->thumb_height = 0; + $this->thumb_width = 0; + } + + if (array_intersect($this->changed, array("parent_id", "name", "slug"))) { + $original->_build_relative_caches(); + $this->relative_path_cache = null; + $this->relative_url_cache = null; + } + + $this->_check_and_fix_conflicts(); + + parent::save(); + + // Now update the filesystem and any database caches if there were significant value + // changes. If anything past this point fails, then we'll have an inconsistent database + // so this code should be as robust as we can make it. + + // Update the MPTT pointers, if necessary. We have to do this before we generate any + // cached paths! + if ($original->parent_id != $this->parent_id) { + parent::move_to($this->parent()); + } + + if ($original->parent_id != $this->parent_id || $original->name != $this->name) { + $this->_build_relative_caches(); + // If there is a data file, then we want to preserve both the old data and the new data. + // (Third-party event handlers would like access to both). The old data file will be + // accessible via the $original item, and the new one via $this item. But in that case, + // we don't want to rename the original as below, because the old data would end up being + // clobbered by the new data file. Also, the rename isn't necessary, because the new item + // data is coming from the data file anyway. So we only perform the rename if there isn't + // a data file. Another way to solve this would be to copy the original file rather than + // conditionally rename it, but a copy would cost far more than the rename. + if (!isset($this->data_file)) { + @rename($original->file_path(), $this->file_path()); + } + // Move all of the items associated data files + if ($this->is_album()) { + @rename(dirname($original->resize_path()), dirname($this->resize_path())); + @rename(dirname($original->thumb_path()), dirname($this->thumb_path())); + } else { + @rename($original->resize_path(), $this->resize_path()); + @rename($original->thumb_path(), $this->thumb_path()); + } + + if ($original->parent_id != $this->parent_id) { + // This will result in 2 events since we'll still fire the item_updated event below + module::event("item_moved", $this, $original->parent()); + } + } + + // Changing the name, slug or parent ripples downwards + if ($this->is_album() && + ($original->name != $this->name || + $original->slug != $this->slug || + $original->parent_id != $this->parent_id)) { + db::build() + ->update("items") + ->set("relative_url_cache", null) + ->set("relative_path_cache", null) + ->where("left_ptr", ">", $this->left_ptr) + ->where("right_ptr", "<", $this->right_ptr) + ->execute(); + } + + // Replace the data file, if requested. + if ($this->data_file && ($this->is_photo() || $this->is_movie())) { + copy($this->data_file, $this->file_path()); + $this->thumb_dirty = 1; + $this->resize_dirty = 1; + } + + module::event("item_updated", $original, $this); + + if ($this->data_file) { + // Null out the data file variable here, otherwise this event will trigger another + // save() which will think that we're doing another file move. + $this->data_file = null; + if ($original->file_path() != $this->file_path()) { + @unlink($original->file_path()); + } + module::event("item_updated_data_file", $this); + } + } + } else if (!empty($this->changed)) { + // Insignificant changes only. Don't fire events or do any special checking to try to keep + // this lightweight. + parent::save(); + } + + return $this; + } + + /** + * Check to see if there's another item that occupies the same name or slug that this item + * intends to use, and if so choose a new name/slug while preserving the extension. Since this + * checks the name without its extension, it covers possible collisions with thumbs and resizes + * as well (e.g. between the thumbs of movie "foo.flv" and photo "foo.jpg"). + */ + private function _check_and_fix_conflicts() { + $suffix_num = 1; + $suffix = ""; + if ($this->is_album()) { + while (db::build() + ->from("items") + ->where("parent_id", "=", $this->parent_id) + ->where("id", $this->id ? "<>" : "IS NOT", $this->id) + ->and_open() + ->where("name", "=", "{$this->name}{$suffix}") + ->or_where("slug", "=", "{$this->slug}{$suffix}") + ->close() + ->count_records()) { + $suffix = "-" . (($suffix_num <= 99) ? sprintf("%02d", $suffix_num++) : random::int()); + } + if ($suffix) { + $this->name = "{$this->name}{$suffix}"; + $this->slug = "{$this->slug}{$suffix}"; + $this->relative_path_cache = null; + $this->relative_url_cache = null; + } + } else { + // Split the filename into its base and extension. This uses a regexp similar to + // legal_file::change_extension (which isn't always the same as pathinfo). + if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $this->name, $matches)) { + $base_name = $matches[1]; + $extension = $matches[2]; // includes a leading dot + } else { + $base_name = $this->name; + $extension = ""; + } + $base_name_escaped = Database::escape_for_like($base_name); + // Note: below query uses LIKE with wildcard % at end, which is still sargable (i.e. quick) + while (db::build() + ->from("items") + ->where("parent_id", "=", $this->parent_id) + ->where("id", $this->id ? "<>" : "IS NOT", $this->id) + ->and_open() + ->where("name", "LIKE", "{$base_name_escaped}{$suffix}.%") + ->or_where("slug", "=", "{$this->slug}{$suffix}") + ->close() + ->count_records()) { + $suffix = "-" . (($suffix_num <= 99) ? sprintf("%02d", $suffix_num++) : random::int()); + } + if ($suffix) { + $this->name = "{$base_name}{$suffix}{$extension}"; + $this->slug = "{$this->slug}{$suffix}"; + $this->relative_path_cache = null; + $this->relative_url_cache = null; + } + } + } + + /** + * Process the data file info. Get its metadata and extension. + * If valid, use it to sanitize the item name and update the + * width, height, and mime type. + */ + private function _process_data_file_info() { + try { + if ($this->is_photo()) { + list ($this->width, $this->height, $this->mime_type, $extension) = + photo::get_file_metadata($this->data_file); + } else if ($this->is_movie()) { + list ($this->width, $this->height, $this->mime_type, $extension) = + movie::get_file_metadata($this->data_file); + } else { + // Albums don't have data files. + $this->data_file = null; + return; + } + + // Sanitize the name based on the idenified extension, but only set $this->name if different + // to ensure it isn't unnecessarily marked as "changed" + $name = legal_file::sanitize_filename($this->name, $extension, $this->type); + if ($this->name != $name) { + $this->name = $name; + } + + // Data file valid - make sure the flag is reset to false. + $this->data_file_error = false; + } catch (Exception $e) { + // Data file invalid - set the flag so it's reported during item validation. + $this->data_file_error = true; + } + } + + /** + * Return the Item_Model representing the cover for this album. + * @return Item_Model or null if there's no cover + */ + public function album_cover() { + if (!$this->is_album()) { + return null; + } + + if (empty($this->album_cover_item_id)) { + return null; + } + + try { + return model_cache::get("item", $this->album_cover_item_id); + } catch (Exception $e) { + // It's possible (unlikely) that the item was deleted, if so keep going. + return null; + } + } + + /** + * Find the position of the given child id in this album. The resulting value is 1-indexed, so + * the first child in the album is at position 1. + * + * This method stands as a backward compatibility for gallery 3.0, and will + * be deprecated in version 3.1. + */ + public function get_position($child, $where=array()) { + return item::get_position($child, $where); + } + + /** + * Return an <img> tag for the thumbnail. + * @param array $extra_attrs Extra attributes to add to the img tag + * @param int (optional) $max Maximum size of the thumbnail (default: null) + * @param boolean (optional) $center_vertically Center vertically (default: false) + * @return string + */ + public function thumb_img($extra_attrs=array(), $max=null, $center_vertically=false) { + list ($height, $width) = $this->scale_dimensions($max); + if ($center_vertically && $max) { + // The constant is divide by 2 to calculate the file and 10 to convert to em + $margin_top = (int)(($max - $height) / 20); + $extra_attrs["style"] = "margin-top: {$margin_top}em"; + $extra_attrs["title"] = $this->title; + } + $attrs = array_merge($extra_attrs, + array( + "src" => $this->thumb_url(), + "alt" => $this->title, + "width" => $width, + "height" => $height) + ); + // html::image forces an absolute url which we don't want + return "<img" . html::attributes($attrs) . "/>"; + } + + /** + * Calculate the largest width/height that fits inside the given maximum, while preserving the + * aspect ratio. Don't upscale. + * @param int $max Maximum size of the largest dimension + * @return array + */ + public function scale_dimensions($max) { + $width = $this->thumb_width; + $height = $this->thumb_height; + + if ($width <= $max && $height <= $max) { + return array($height, $width); + } + + if ($height) { + if (isset($max)) { + if ($width > $height) { + $height = (int)($max * $height / $width); + $width = $max; + } else { + $width = (int)($max * $width / $height); + $height = $max; + } + } + } else { + // Missing thumbnail, can happen on albums with no photos yet. + // @todo we should enforce a placeholder for those albums. + $width = 0; + $height = 0; + } + return array($height, $width); + } + + /** + * Return an <img> tag for the resize. + * @param array $extra_attrs Extra attributes to add to the img tag + * @return string + */ + public function resize_img($extra_attrs) { + $attrs = array_merge($extra_attrs, + array("src" => $this->resize_url(), + "alt" => $this->title, + "width" => $this->resize_width, + "height" => $this->resize_height) + ); + // html::image forces an absolute url which we don't want + return "<img" . html::attributes($attrs) . "/>"; + } + + /** + * Return a view for movies. By default this is a Flowplayer v3 <script> tag, but + * movie_img events can override this and provide their own player/view. If no player/view + * is found and the movie is unsupported by Flowplayer v3, this returns a simple download link. + * @param array $extra_attrs + * @return string + */ + public function movie_img($extra_attrs) { + $max_size = module::get_var("gallery", "resize_size", 640); + $width = $this->width; + $height = $this->height; + if ($width == 0 || $height == 0) { + // Not set correctly, likely because ffmpeg isn't available. Making the window 0x0 causes the + // video to be effectively unviewable. So, let's guess: set width to max_size and guess a + // height (using 4:3 aspect ratio). Once the video metadata is loaded, js in + // movieplayer.html.php will correct these values. + $width = $max_size; + $height = ceil($width * 3/4); + } + $attrs = array_merge(array("id" => "g-item-id-{$this->id}"), $extra_attrs, + array("class" => "g-movie")); + + // Run movie_img events, which can either: + // - generate a view, which is used in place of the standard Flowplayer v3 player + // (use view variable) + // - alter the arguments sent to the standard player + // (use fp_params and fp_config variables) + $movie_img = new stdClass(); + $movie_img->max_size = $max_size; + $movie_img->width = $width; + $movie_img->height = $height; + $movie_img->attrs = $attrs; + $movie_img->url = $this->file_url(true); + $movie_img->filename = $this->name; + $movie_img->fp_params = array(); // additional Flowplayer params values (will be json encoded) + $movie_img->fp_config = array(); // additional Flowplayer config values (will be json encoded) + $movie_img->view = array(); + module::event("movie_img", $movie_img, $this); + + if (count($movie_img->view) > 0) { + // View generated - use it + $view = implode("\n", $movie_img->view); + } else { + // View NOT generated - see if filetype supported by Flowplayer v3 + // Note that the extension list below is hard-coded and doesn't use the legal_file helper + // since anything else will not work in Flowplayer v3. + if (in_array(strtolower(pathinfo($movie_img->filename, PATHINFO_EXTENSION)), + array("flv", "mp4", "m4v", "mov", "f4v"))) { + // Filetype supported by Flowplayer v3 - use it (default) + $view = new View("movieplayer.html"); + $view->max_size = $movie_img->max_size; + $view->width = $movie_img->width; + $view->height = $movie_img->height; + $view->attrs = $movie_img->attrs; + $view->url = $movie_img->url; + $view->fp_params = $movie_img->fp_params; + $view->fp_config = $movie_img->fp_config; + } else { + // Filetype NOT supported by Flowplayer v3 - display download link + $attrs = array_merge($attrs, array("style" => "width: {$max_size}px;", + "download" => $this->name, // forces download (HTML5 only) + "class" => "g-movie g-movie-download-link")); + $view = html::anchor($this->file_url(true), t("Click here to download item."), $attrs); + } + } + return $view; + } + + /** + * Return all of the children of this album. Unless you specify a specific sort order, the + * results will be ordered by this album's sort order. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @param array additional where clauses + * @param array order_by + * @return array ORM + */ + function children($limit=null, $offset=null, $where=array(), $order_by=null) { + if (empty($order_by)) { + $order_by = array($this->sort_column => $this->sort_order); + // Use id as a tie breaker + if ($this->sort_column != "id") { + $order_by["id"] = "ASC"; + } + } + return parent::children($limit, $offset, $where, $order_by); + } + + /** + * Return the children of this album, and all of it's sub-albums. Unless you specify a specific + * sort order, the results will be ordered by this album's sort order. Note that this + * album's sort order is imposed on all sub-albums, regardless of their sort order. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @param array additional where clauses + * @return object ORM_Iterator + */ + function descendants($limit=null, $offset=null, $where=array(), $order_by=null) { + if (empty($order_by)) { + $order_by = array($this->sort_column => $this->sort_order); + // Use id as a tie breaker + if ($this->sort_column != "id") { + $order_by["id"] = "ASC"; + } + } + return parent::descendants($limit, $offset, $where, $order_by); + } + + /** + * Specify our rules here so that we have access to the instance of this model. + */ + public function validate(Validation $array=null) { + if (!$array) { + $this->rules = array( + "album_cover_item_id" => array("callbacks" => array(array($this, "valid_album_cover"))), + "description" => array("rules" => array("length[0,65535]")), + "mime_type" => array("callbacks" => array(array($this, "valid_field"))), + "name" => array("rules" => array("length[0,255]", "required"), + "callbacks" => array(array($this, "valid_name"))), + "parent_id" => array("callbacks" => array(array($this, "valid_parent"))), + "rand_key" => array("rule" => array("decimal")), + "slug" => array("rules" => array("length[0,255]", "required"), + "callbacks" => array(array($this, "valid_slug"))), + "sort_column" => array("callbacks" => array(array($this, "valid_field"))), + "sort_order" => array("callbacks" => array(array($this, "valid_field"))), + "title" => array("rules" => array("length[0,255]", "required")), + "type" => array("callbacks" => array(array($this, "read_only"), + array($this, "valid_field"))), + ); + + // Conditional rules + if ($this->id == 1) { + // We don't care about the name and slug for the root album. + $this->rules["name"] = array(); + $this->rules["slug"] = array(); + } + + // Movies and photos must have data files. Verify the data file on new items, or if it has + // been replaced. + if (($this->is_photo() || $this->is_movie()) && $this->data_file) { + $this->rules["name"]["callbacks"][] = array($this, "valid_data_file"); + } + } + + parent::validate($array); + } + + /** + * Validate that the desired slug does not conflict. + */ + public function valid_slug(Validation $v, $field) { + if (preg_match("/[^A-Za-z0-9-_]/", $this->slug)) { + $v->add_error("slug", "not_url_safe"); + } else if (db::build() + ->from("items") + ->where("parent_id", "=", $this->parent_id) + ->where("id", "<>", $this->id) + ->where("slug", "=", $this->slug) + ->count_records()) { + $v->add_error("slug", "conflict"); + } + } + + /** + * Validate the item name. It can't conflict with other names, can't contain slashes or + * trailing periods. + */ + public function valid_name(Validation $v, $field) { + if (strpos($this->name, "/") !== false) { + $v->add_error("name", "no_slashes"); + return; + } + + if (rtrim($this->name, ".") !== $this->name) { + $v->add_error("name", "no_trailing_period"); + return; + } + + // Do not accept files with double extensions, they can cause problems on some + // versions of Apache. + if (!$this->is_album() && substr_count($this->name, ".") > 1) { + $v->add_error("name", "illegal_data_file_extension"); + } + + if ($this->is_movie() || $this->is_photo()) { + $ext = pathinfo($this->name, PATHINFO_EXTENSION); + + if (!$this->loaded() && !$ext) { + // New items must have an extension + $v->add_error("name", "illegal_data_file_extension"); + return; + } + + if ($this->is_photo() && !legal_file::get_photo_extensions($ext) || + $this->is_movie() && !legal_file::get_movie_extensions($ext)) { + $v->add_error("name", "illegal_data_file_extension"); + } + } + + if ($this->is_album()) { + if (db::build() + ->from("items") + ->where("parent_id", "=", $this->parent_id) + ->where("name", "=", $this->name) + ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null) + ->count_records()) { + $v->add_error("name", "conflict"); + return; + } + } else { + if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $this->name, $matches)) { + $base_name = $matches[1]; + } else { + $base_name = $this->name; + } + $base_name_escaped = Database::escape_for_like($base_name); + if (db::build() + ->from("items") + ->where("parent_id", "=", $this->parent_id) + ->where("name", "LIKE", "{$base_name_escaped}.%") + ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null) + ->count_records()) { + $v->add_error("name", "conflict"); + return; + } + } + + if ($this->parent_id == 1 && Kohana::auto_load("{$this->slug}_Controller")) { + $v->add_error("slug", "reserved"); + return; + } + } + + /** + * Make sure that the data file is well formed (it exists and isn't empty). + */ + public function valid_data_file(Validation $v, $field) { + if (!is_file($this->data_file)) { + $v->add_error("name", "bad_data_file_path"); + } else if (filesize($this->data_file) == 0) { + $v->add_error("name", "empty_data_file"); + } else if ($this->data_file_error) { + $v->add_error("name", "invalid_data_file"); + } + } + + /** + * Make sure that the parent id refers to an album. + */ + public function valid_parent(Validation $v, $field) { + if ($this->id == 1) { + if ($this->parent_id != 0) { + $v->add_error("parent_id", "invalid"); + } + } else { + $query = db::build() + ->from("items") + ->where("id", "=", $this->parent_id) + ->where("type", "=", "album"); + + // If this is an existing item, make sure the new parent is not part of our hierarchy + if ($this->loaded()) { + $query->and_open() + ->where("left_ptr", "<", $this->left_ptr) + ->or_where("right_ptr", ">", $this->right_ptr) + ->close(); + } + + if ($query->count_records() != 1) { + $v->add_error("parent_id", "invalid"); + } + } + } + + /** + * Make sure the album cover item id refers to a valid item, or is null. + */ + public function valid_album_cover(Validation $v, $field) { + if ($this->id == 1) { + return; + } + + if ($this->album_cover_item_id && ($this->is_photo() || $this->is_movie() || + db::build() + ->from("items") + ->where("id", "=", $this->album_cover_item_id) + ->where("type", "<>", "album") + ->count_records() != 1)) { + $v->add_error("album_cover_item_id", "invalid_item"); + } + } + + /** + * Make sure that the type is valid. + */ + public function valid_field(Validation $v, $field) { + switch($field) { + case "mime_type": + if ($this->is_movie()) { + $legal_values = legal_file::get_movie_types(); + } else if ($this->is_photo()) { + $legal_values = legal_file::get_photo_types(); + } + break; + + case "sort_column": + if (!array_key_exists($this->sort_column, $this->object)) { + $v->add_error($field, "invalid"); + } + break; + + case "sort_order": + $legal_values = array("ASC", "DESC", "asc", "desc"); + break; + + case "type": + $legal_values = array("album", "photo", "movie"); + break; + + default: + $v->add_error($field, "unvalidated_field"); + break; + } + + if (isset($legal_values) && !in_array($this->$field, $legal_values)) { + $v->add_error($field, "invalid"); + } + } + + /** + * This field cannot be changed after it's been set. + */ + public function read_only(Validation $v, $field) { + if ($this->loaded() && isset($this->changed[$field])) { + $v->add_error($field, "read_only"); + } + } + + /** + * Same as ORM::as_array() but convert id fields into their RESTful form. + * + * @param array if specified, only return the named fields + */ + public function as_restful_array($fields=array()) { + if ($fields) { + $data = array(); + foreach ($fields as $field) { + if (isset($this->object[$field])) { + $data[$field] = $this->__get($field); + } + } + $fields = array_flip($fields); + } else { + $data = $this->as_array(); + } + + // Convert item ids to rest URLs for consistency + if (empty($fields) || isset($fields["parent"])) { + if ($tmp = $this->parent()) { + $data["parent"] = rest::url("item", $tmp); + } + unset($data["parent_id"]); + } + + if (empty($fields) || isset($fields["album_cover"])) { + if ($tmp = $this->album_cover()) { + $data["album_cover"] = rest::url("item", $tmp); + } + unset($data["album_cover_item_id"]); + } + + if (empty($fields) || isset($fields["web_url"])) { + $data["web_url"] = $this->abs_url(); + } + + if (!$this->is_album()) { + if (access::can("view_full", $this)) { + if (empty($fields) || isset($fields["file_url"])) { + $data["file_url"] = rest::url("data", $this, "full"); + } + if (empty($fields) || isset($fields["file_size"])) { + $data["file_size"] = filesize($this->file_path()); + } + if (access::user_can(identity::guest(), "view_full", $this)) { + if (empty($fields) || isset($fields["file_url_public"])) { + $data["file_url_public"] = $this->file_url(true); + } + } + } + } + + if ($this->is_photo()) { + if (empty($fields) || isset($fields["resize_url"])) { + $data["resize_url"] = rest::url("data", $this, "resize"); + } + if (empty($fields) || isset($fields["resize_size"])) { + $data["resize_size"] = filesize($this->resize_path()); + } + if (access::user_can(identity::guest(), "view", $this)) { + if (empty($fields) || isset($fields["resize_url_public"])) { + $data["resize_url_public"] = $this->resize_url(true); + } + } + } + + if ($this->has_thumb()) { + if (empty($fields) || isset($fields["thumb_url"])) { + $data["thumb_url"] = rest::url("data", $this, "thumb"); + } + if (empty($fields) || isset($fields["thumb_size"])) { + $data["thumb_size"] = filesize($this->thumb_path()); + } + if (access::user_can(identity::guest(), "view", $this)) { + if (empty($fields) || isset($fields["thumb_url_public"])) { + $data["thumb_url_public"] = $this->thumb_url(true); + } + } + } + + if (empty($fields) || isset($fields["can_edit"])) { + $data["can_edit"] = access::can("edit", $this); + } + + if (empty($fields) || isset($fields["can_add"])) { + $data["can_add"] = access::can("add", $this); + } + + // Elide some internal-only data that is going to cause confusion in the client. + foreach (array("relative_path_cache", "relative_url_cache", "left_ptr", "right_ptr", + "thumb_dirty", "resize_dirty", "weight") as $key) { + unset($data[$key]); + } + return $data; + } + + /** + * Increments the view counter of this item + * We can't use math in ORM or the query builder, so do this by hand. It's important + * that we do this with math, otherwise concurrent accesses will damage accuracy. + */ + public function increment_view_count() { + db::query("UPDATE {items} SET `view_count` = `view_count` + 1 WHERE `id` = $this->id") + ->execute(); + } + + private function _cache_buster($path) { + return "?m=" . (string)(file_exists($path) ? filemtime($path) : 0); + } +} diff --git a/modules/gallery/models/log.php b/modules/gallery/models/log.php new file mode 100644 index 0000000..b01bcd2 --- /dev/null +++ b/modules/gallery/models/log.php @@ -0,0 +1,38 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Log_Model_Core extends ORM { + /** + * @see ORM::__get() + */ + public function __get($column) { + if ($column == "user") { + // This relationship depends on an outside module, which may not be present so handle + // failures gracefully. + try { + return identity::lookup_user($this->user_id); + } catch (Exception $e) { + Kohana_Log::add("alert", "Unable to load user with id $this->user_id"); + return null; + } + } else { + return parent::__get($column); + } + } +} diff --git a/modules/gallery/models/message.php b/modules/gallery/models/message.php new file mode 100644 index 0000000..0fa803d --- /dev/null +++ b/modules/gallery/models/message.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Message_Model_Core extends ORM { +} diff --git a/modules/gallery/models/module.php b/modules/gallery/models/module.php new file mode 100644 index 0000000..0b2fb77 --- /dev/null +++ b/modules/gallery/models/module.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Module_Model_Core extends ORM { +} diff --git a/modules/gallery/models/outgoing_translation.php b/modules/gallery/models/outgoing_translation.php new file mode 100644 index 0000000..66e2091 --- /dev/null +++ b/modules/gallery/models/outgoing_translation.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Outgoing_Translation_Model_Core extends ORM { +} diff --git a/modules/gallery/models/permission.php b/modules/gallery/models/permission.php new file mode 100644 index 0000000..26d1ee7 --- /dev/null +++ b/modules/gallery/models/permission.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Permission_Model_Core extends ORM { +} diff --git a/modules/gallery/models/task.php b/modules/gallery/models/task.php new file mode 100644 index 0000000..31332ba --- /dev/null +++ b/modules/gallery/models/task.php @@ -0,0 +1,86 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Task_Model_Core extends ORM { + public function get($key, $default=null) { + $context = unserialize($this->context); + if (array_key_exists($key, $context)) { + return $context[$key]; + } else { + return $default; + } + } + + public function set($key, $value=null) { + $context = unserialize($this->context); + $context[$key] = $value; + $this->context = serialize($context); + } + + public function save() { + if (!empty($this->changed)) { + $this->updated = time(); + } + return parent::save(); + } + + public function delete($ignored_id=null) { + Cache::instance()->delete($this->_cache_key()); + return parent::delete(); + } + + public function owner() { + return identity::lookup_user($this->owner_id); + } + + /** + * Log a message to the task log. + * @params $msg mixed a string or array of strings + */ + public function log($msg) { + $key = $this->_cache_key(); + $log = Cache::instance()->get($key); + + if (is_array($msg)) { + $msg = implode("\n", $msg); + } + + // Save for 30 days. + $log .= !empty($log) ? "\n" : ""; + Cache::instance()->set($key, "$log{$msg}", + array("task", "log", "import"), 2592000); + } + + /** + * Retrieve the cached log information for this task. + * @returns the log data or null if there is no log data + */ + public function get_log() { + $log_data = Cache::instance()->get($this->_cache_key()); + return $log_data !== null ? $log_data : false; + } + + /** + * Build the task cache key + * @returns the key to use in access the cache + */ + private function _cache_key() { + return md5("$this->id; $this->name; $this->callback"); + } +}
\ No newline at end of file diff --git a/modules/gallery/models/theme.php b/modules/gallery/models/theme.php new file mode 100644 index 0000000..2e4d932 --- /dev/null +++ b/modules/gallery/models/theme.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Theme_Model_Core extends ORM { +}
\ No newline at end of file diff --git a/modules/gallery/models/var.php b/modules/gallery/models/var.php new file mode 100644 index 0000000..7049e7d --- /dev/null +++ b/modules/gallery/models/var.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Var_Model_Core extends ORM { +} diff --git a/modules/gallery/module.info b/modules/gallery/module.info new file mode 100644 index 0000000..7f49b72 --- /dev/null +++ b/modules/gallery/module.info @@ -0,0 +1,7 @@ +name = "Gallery 3" +description = "Gallery core application" +version = 57 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:gallery" +discuss_url = "http://galleryproject.org/forum_module_gallery" diff --git a/modules/gallery/vendor/joomla/crypt.php b/modules/gallery/vendor/joomla/crypt.php new file mode 100644 index 0000000..c7d477d --- /dev/null +++ b/modules/gallery/vendor/joomla/crypt.php @@ -0,0 +1,151 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * @package Joomla.Platform + * @subpackage Crypt + * + * @copyright Copyright (C) 2005 - 2011 Open Source Matters, Inc. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE + */ + +// defined('JPATH_PLATFORM') or die; + +/** + * JCrypt is a Joomla Platform class for handling basic encryption/decryption of data. + * + * @package Joomla.Platform + * @subpackage Crypt + * @since 12.1 + */ +class JCrypt +{ + /** + * Generate random bytes. + * + * @param integer $length Length of the random data to generate + * + * @return string Random binary data + * + * @since 12.1 + */ + public static function genRandomBytes($length = 16) + { + $sslStr = ''; + /* + * if a secure randomness generator exists and we don't + * have a buggy PHP version use it. + */ + if ( + function_exists('openssl_random_pseudo_bytes') + && (version_compare(PHP_VERSION, '5.3.4') >= 0 + || substr(PHP_OS, 0, 3) !== 'WIN' + ) + ) + { + $sslStr = openssl_random_pseudo_bytes($length, $strong); + if ($strong) + { + return $sslStr; + } + } + + /* + * Collect any entropy available in the system along with a number + * of time measurements of operating system randomness. + */ + $bitsPerRound = 2; + $maxTimeMicro = 400; + $shaHashLength = 20; + $randomStr = ''; + $total = $length; + + // Check if we can use /dev/urandom. + $urandom = false; + $handle = null; + if (function_exists('stream_set_read_buffer') && @is_readable('/dev/urandom')) + { + $handle = @fopen('/dev/urandom', 'rb'); + if ($handle) + { + $urandom = true; + } + } + + while ($length > strlen($randomStr)) + { + $bytes = ($total > $shaHashLength)? $shaHashLength : $total; + $total -= $bytes; + /* + * Collect any entropy available from the PHP system and filesystem. + * If we have ssl data that isn't strong, we use it once. + */ + $entropy = rand() . uniqid(mt_rand(), true) . $sslStr; + $entropy .= implode('', @fstat(fopen( __FILE__, 'r'))); + $entropy .= memory_get_usage(); + $sslStr = ''; + if ($urandom) + { + stream_set_read_buffer($handle, 0); + $entropy .= @fread($handle, $bytes); + } + else + { + /* + * There is no external source of entropy so we repeat calls + * to mt_rand until we are assured there's real randomness in + * the result. + * + * Measure the time that the operations will take on average. + */ + $samples = 3; + $duration = 0; + for ($pass = 0; $pass < $samples; ++$pass) + { + $microStart = microtime(true) * 1000000; + $hash = sha1(mt_rand(), true); + for ($count = 0; $count < 50; ++$count) + { + $hash = sha1($hash, true); + } + $microEnd = microtime(true) * 1000000; + $entropy .= $microStart . $microEnd; + if ($microStart > $microEnd) { + $microEnd += 1000000; + } + $duration += $microEnd - $microStart; + } + $duration = $duration / $samples; + + /* + * Based on the average time, determine the total rounds so that + * the total running time is bounded to a reasonable number. + */ + $rounds = (int)(($maxTimeMicro / $duration) * 50); + + /* + * Take additional measurements. On average we can expect + * at least $bitsPerRound bits of entropy from each measurement. + */ + $iter = $bytes * (int) ceil(8 / $bitsPerRound); + for ($pass = 0; $pass < $iter; ++$pass) + { + $microStart = microtime(true); + $hash = sha1(mt_rand(), true); + for ($count = 0; $count < $rounds; ++$count) + { + $hash = sha1($hash, true); + } + $entropy .= $microStart . microtime(true); + } + } + + $randomStr .= sha1($entropy, true); + } + + if ($urandom) + { + @fclose($handle); + } + + return substr($randomStr, 0, $length); + } +} diff --git a/modules/gallery/views/admin_advanced_settings.html.php b/modules/gallery/views/admin_advanced_settings.html.php new file mode 100644 index 0000000..6745f0d --- /dev/null +++ b/modules/gallery/views/admin_advanced_settings.html.php @@ -0,0 +1,57 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-admin-advanced-settings" class="g-block"> + <h1> <?= t("Advanced settings") ?> </h1> + <p> + <?= t("Here are internal Gallery configuration settings. Most of these settings are accessible elsewhere in the administrative console.") ?> + </p> + + <ul id="g-action-status" class="g-message-block"> + <li class="g-warning"><?= t("Change these values at your own risk!") ?></li> + </ul> + + <?= t("Filter:") ?> <input id="g-admin-advanced-settings-filter" type="text"></input> + <div class="g-block-content"> + <table> + <tr> + <th> <?= t("Module") ?> </th> + <th> <?= t("Name") ?> </th> + <th> <?= t("Value") ?></th> + </tr> + <? foreach ($vars as $var): ?> + <tr class="setting-row <?= text::alternate("g-odd", "g-even") ?>"> + <td> <?= html::clean($var->module_name) ?> </td> + <td> <?= html::clean($var->name) ?> </td> + <td> + <a href="<?= url::site("admin/advanced_settings/edit/$var->module_name/" . html::clean($var->name)) ?>" + class="g-dialog-link" + title="<?= t("Edit %var (%module_name)", array("var" => $var->name, "module_name" => $var->module_name))->for_html_attr() ?>"> + <? if (!isset($var->value) || $var->value === ""): ?> + <i> <?= t("empty") ?> </i> + <? else: ?> + <?= html::clean($var->value) ?> + <? endif ?> + </a> + </td> + </tr> + <? endforeach ?> + </table> + </div> + + <script> + $(document).ready(function() { + $("#g-admin-advanced-settings-filter").keyup(function() { + var filter = $(this).attr("value"); + if (filter) { + $("tr.setting-row").fadeOut("fast"); + $("tr.setting-row").each(function() { + if ($(this).text().indexOf(filter) > 0) { + $(this).stop().show(); + } + }); + } else { + $("tr.setting-row").show(); + } + }); + }); + </script> +</div> diff --git a/modules/gallery/views/admin_block_log_entries.html.php b/modules/gallery/views/admin_block_log_entries.html.php new file mode 100644 index 0000000..5a8ed23 --- /dev/null +++ b/modules/gallery/views/admin_block_log_entries.html.php @@ -0,0 +1,15 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <? foreach ($entries as $entry): ?> + <li class="<?= log::severity_class($entry->severity) ?>" style="direction: ltr"> + <? if ($entry->user->guest): ?> + </span><?= html::clean($entry->user->name) ?></span> + <? else: ?> + <a href="<?= user_profile::url($entry->user->id) ?>"><?= html::clean($entry->user->name) ?></a> + <? endif ?> + <?= gallery::date_time($entry->timestamp) ?> + <?= $entry->message ?> + <?= $entry->html ?> + </li> + <? endforeach ?> +</ul> diff --git a/modules/gallery/views/admin_block_news.html.php b/modules/gallery/views/admin_block_news.html.php new file mode 100644 index 0000000..cb276ae --- /dev/null +++ b/modules/gallery/views/admin_block_news.html.php @@ -0,0 +1,11 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <? foreach ($feed as $entry): ?> + <li> + <a href="<?= $entry["link"] ?>"><?= $entry["title"] ?></a> + <p> + <?= text::limit_words(strip_tags($entry["description"]), 25); ?> + </p> + </li> + <? endforeach ?> +</ul> diff --git a/modules/gallery/views/admin_block_photo_stream.html.php b/modules/gallery/views/admin_block_photo_stream.html.php new file mode 100644 index 0000000..f9725ee --- /dev/null +++ b/modules/gallery/views/admin_block_photo_stream.html.php @@ -0,0 +1,14 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> +<? foreach ($photos as $photo): ?> + <li class="g-item g-photo"> + <a href="<?= $photo->url() ?>" title="<?= html::purify($photo->title)->for_html_attr() ?>"> + <img <?= photo::img_dimensions($photo->width, $photo->height, 72) ?> + src="<?= $photo->thumb_url() ?>" alt="<?= html::purify($photo->title)->for_html_attr() ?>" /> + </a> + </li> +<? endforeach ?> +</ul> +<p> + <?= t("Recent photos added to your Gallery") ?> +</p> diff --git a/modules/gallery/views/admin_block_platform.html.php b/modules/gallery/views/admin_block_platform.html.php new file mode 100644 index 0000000..9a594fa --- /dev/null +++ b/modules/gallery/views/admin_block_platform.html.php @@ -0,0 +1,24 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <li> + <?= t("Host name: %host_name", array("host_name" => php_uname("n"))) ?> + </li> + <li> + <?= t("Operating system: %os %version", array("os" => php_uname("s"), "version" => php_uname("r"))) ?> + </li> + <li> + <?= t("Apache: %apache_version", array("apache_version" => function_exists("apache_get_version") ? apache_get_version() : t("Unknown"))) ?> + </li> + <li> + <?= t("PHP: %php_version", array("php_version" => phpversion())) ?> + </li> + <li> + <?= t("MySQL: %mysql_version", array("mysql_version" => Database::instance()->query("SELECT version() as v")->current()->v)) ?> + </li> + <li> + <?= t("Server load: %load_average", array("load_average" => join(" ", sys_getloadavg()))) ?> + </li> + <li> + <?= t("Graphics toolkit: %toolkit", array("toolkit" => module::get_var("gallery", "graphics_toolkit"))) ?> + </li> +</ul> diff --git a/modules/gallery/views/admin_block_stats.html.php b/modules/gallery/views/admin_block_stats.html.php new file mode 100644 index 0000000..c8c5476 --- /dev/null +++ b/modules/gallery/views/admin_block_stats.html.php @@ -0,0 +1,12 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <li> + <?= t("Version: %version", array("version" => gallery::version_string())) ?> + </li> + <li> + <?= t("Albums: %count", array("count" => $album_count)) ?> + </li> + <li> + <?= t("Photos: %count", array("count" => $photo_count)) ?> + </li> +</ul> diff --git a/modules/gallery/views/admin_block_welcome.html.php b/modules/gallery/views/admin_block_welcome.html.php new file mode 100644 index 0000000..d3765d1 --- /dev/null +++ b/modules/gallery/views/admin_block_welcome.html.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<p> + <?= t("This is your administration dashboard and it provides a quick overview of status messages, recent updates, and frequently used options. Add or remove blocks and rearrange them to tailor to your needs. The admin menu provides quick access to all of Gallery 3's options and settings. Here are a few of the most used options to get you started.") ?> +</p> +<ul class="g-text"> + <li> + <?= t("General Settings - choose your <a href=\"%graphics_url\">graphics</a> and <a href=\"%language_url\">language</a> settings.", + array("graphics_url" => html::mark_clean(url::site("admin/graphics")), + "language_url" => html::mark_clean(url::site("admin/languages")))) ?> + </li> + <li> + <?= t("Appearance - <a href=\"%theme_url\">choose a theme</a>, or <a href=\"%theme_options_url\">customize the way it looks</a>.", + array("theme_url" => html::mark_clean(url::site("admin/themes")), + "theme_options_url" => html::mark_clean(url::site("admin/theme_options")))) ?> + </li> + <li> + <?= t("Customize - <a href=\"%modules_url\">install modules</a> to add cool features!", + array("modules_url" => html::mark_clean(url::site("admin/modules")))) ?> + </li> +</ul> diff --git a/modules/gallery/views/admin_dashboard.html.php b/modules/gallery/views/admin_dashboard.html.php new file mode 100644 index 0000000..cf90ef2 --- /dev/null +++ b/modules/gallery/views/admin_dashboard.html.php @@ -0,0 +1,43 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + update_blocks = function() { + $.get(<?= html::js_string(url::site("admin/dashboard/reorder")) ?>, + {"csrf": "<?= $csrf ?>", + "dashboard_center[]": $("#g-admin-dashboard").sortable( + "toArray", {attribute: "block_id"}), + "dashboard_sidebar[]": $("#g-admin-dashboard-sidebar").sortable( + "toArray", {attribute: "block_id"})}); + }; + + $(document).ready(function(){ + $("#g-admin-dashboard .g-block .ui-widget-header").addClass("g-draggable"); + $("#g-admin-dashboard").sortable({ + connectWith: ["#g-admin-dashboard-sidebar"], + cursor: "move", + handle: $(".ui-widget-header"), + opacity: 0.6, + placeholder: "g-target", + stop: update_blocks + }); + + $("#g-admin-dashboard-sidebar .g-block .ui-widget-header").addClass("g-draggable"); + $("#g-admin-dashboard-sidebar").sortable({ + connectWith: ["#g-admin-dashboard"], + cursor: "move", + handle: $(".ui-widget-header"), + opacity: 0.6, + placeholder: "g-target", + stop: update_blocks + }); + }); +</script> +<div> + <? if ($obsolete_modules_message): ?> + <p class="g-warning"> + <?= $obsolete_modules_message ?> + </p> + <? endif ?> +</div> +<div id="g-admin-dashboard"> + <?= $blocks ?> +</div> diff --git a/modules/gallery/views/admin_graphics.html.php b/modules/gallery/views/admin_graphics.html.php new file mode 100644 index 0000000..1f45bb1 --- /dev/null +++ b/modules/gallery/views/admin_graphics.html.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $(document).ready(function() { + $(".g-available .g-block").equal_heights(); + select_toolkit = function(el) { + if (!$(this).hasClass("g-unavailable")) { + window.location = <?= html::js_string(url::site("admin/graphics/choose/__TK__?csrf=$csrf")) ?> + .replace("__TK__", $(this).attr("id")); + } + }; + $("#g-admin-graphics div.g-available .g-block").click(select_toolkit); + }); +</script> + +<div id="g-admin-graphics" class="g-block ui-helper-clearfix"> + <h1> <?= t("Graphics settings") ?> </h1> + <p> + <?= t("Gallery needs a graphics toolkit in order to manipulate your photos. Please choose one from the list below.") ?> + <?= t("Can't decide which toolkit to choose? <a href=\"%url\">We can help!</a>", array("url" => "http://codex.galleryproject.org/Gallery3:Choosing_A_Graphics_Toolkit")) ?> + </p> + + <div class="g-block-content"> + <h2> <?= t("Active toolkit") ?> </h2> + <? if ($active == "none"): ?> + <?= new View("admin_graphics_none.html") ?> + <? else: ?> + <?= new View("admin_graphics_$active.html", array("tk" => $tk->$active, "is_active" => true)) ?> + <? endif ?> + + <div class="g-available"> + <h2> <?= t("Available toolkits") ?> </h2> + <? foreach (array_keys((array)$tk) as $id): ?> + <? if ($id != $active): ?> + <?= new View("admin_graphics_$id.html", array("tk" => $tk->$id, "is_active" => false)) ?> + <? endif ?> + <? endforeach ?> + </div> + </div> +</div> + diff --git a/modules/gallery/views/admin_graphics_gd.html.php b/modules/gallery/views/admin_graphics_gd.html.php new file mode 100644 index 0000000..1cc9dc9 --- /dev/null +++ b/modules/gallery/views/admin_graphics_gd.html.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="gd" class="g-block<?= $is_active ? " g-selected" : "" ?><?= $tk->installed ? " g-installed-toolkit" : " g-unavailable" ?>"> + <img class="logo" width="170" height="110" src="<?= url::file("modules/gallery/images/gd.png"); ?>" alt="<? t("Visit the GD lib project site") ?>" /> + <h3> <?= t("GD") ?> </h3> + <p> + <?= t("The GD graphics library is an extension to PHP commonly installed most webservers. Please refer to the <a href=\"%url\">GD website</a> for more information.", + array("url" => "http://www.boutell.com/gd")) ?> + </p> + <? if ($tk->installed && $tk->rotate): ?> + <div class="g-module-status g-info"> + <?= t("You have GD version %version.", array("version" => $tk->version)) ?> + </div> + <p> + <a class="g-button ui-state-default ui-corner-all"><?= t("Activate GD") ?></a> + </p> + <? elseif ($tk->installed): ?> + <? if ($tk->error): ?> + <p class="g-module-status g-warning"> + <?= $tk->error ?> + </p> + <? endif ?> + <p> + <a class="g-button ui-state-default ui-corner-all"><?= t("Activate GD") ?></a> + </p> + <? else: ?> + <div class="g-module-status g-info"> + <?= t("You do not have GD installed.") ?> + </div> + <? endif ?> +</div> diff --git a/modules/gallery/views/admin_graphics_graphicsmagick.html.php b/modules/gallery/views/admin_graphics_graphicsmagick.html.php new file mode 100644 index 0000000..5dae144 --- /dev/null +++ b/modules/gallery/views/admin_graphics_graphicsmagick.html.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="graphicsmagick" class="g-block<?= $is_active ? " g-selected" : "" ?><?= $tk->installed ? " g-installed-toolkit" : " g-unavailable" ?>"> + <img class="logo" width="107" height="76" src="<?= url::file("modules/gallery/images/graphicsmagick.png"); ?>" alt="<? t("Visit the GraphicsMagick project site") ?>" /> + <h3> <?= t("GraphicsMagick") ?> </h3> + <p> + <?= t("GraphicsMagick is a standalone graphics program available on most Linux systems. Please refer to the <a href=\"%url\">GraphicsMagick website</a> for more information.", + array("url" => "http://www.graphicsmagick.org")) ?> + </p> + <? if ($tk->installed): ?> + <div class="g-module-status g-info"> + <?= t("GraphicsMagick version %version is available in %dir", array("version" => $tk->version, "dir" => $tk->dir)) ?> + </div> + <p> + <a class="g-button ui-state-default ui-corner-all"><?= t("Activate Graphics Magic") ?></a> + </p> + <? else: ?> + <div class="g-module-status g-warning"> + <?= $tk->error ?> + </div> + <? endif ?> +</div> diff --git a/modules/gallery/views/admin_graphics_imagemagick.html.php b/modules/gallery/views/admin_graphics_imagemagick.html.php new file mode 100644 index 0000000..9c1a990 --- /dev/null +++ b/modules/gallery/views/admin_graphics_imagemagick.html.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="imagemagick" class="g-block<?= $is_active ? " g-selected" : "" ?><?= $tk->installed ? " g-installed-toolkit" : " g-unavailable" ?>"> + <img class="logo" width="114" height="118" src="<?= url::file("modules/gallery/images/imagemagick.jpg"); ?>" alt="<? t("Visit the ImageMagick project site") ?>" /> + <h3> <?= t("ImageMagick") ?> </h3> + <p> + <?= t("ImageMagick is a standalone graphics program available on most Linux systems. Please refer to the <a href=\"%url\">ImageMagick website</a> for more information.", + array("url" => "http://www.imagemagick.org")) ?> + </p> + <? if ($tk->installed): ?> + <div class="g-module-status g-info"> + <?= t("ImageMagick version %version is available in %dir", array("version" => $tk->version, "dir" => $tk->dir)) ?> + </div> + <p> + <a class="g-button ui-state-default ui-corner-all"><?= t("Activate ImageMagick") ?></a> + </p> + <? elseif ($tk->error): ?> + <div class="g-module-status g-warning"> + <?= $tk->error ?> + </div> + <? endif ?> +</div> diff --git a/modules/gallery/views/admin_graphics_none.html.php b/modules/gallery/views/admin_graphics_none.html.php new file mode 100644 index 0000000..e0fc417 --- /dev/null +++ b/modules/gallery/views/admin_graphics_none.html.php @@ -0,0 +1,8 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> + +<div id="none" class="g-module-status g-warning g-block"> + <h3> <?= t("No active toolkit") ?> </h3> + <p> + <?= t("We were unable to detect a graphics program. You must install one of the toolkits below in order to use many Gallery features.") ?> + </p> +</div> diff --git a/modules/gallery/views/admin_languages.html.php b/modules/gallery/views/admin_languages.html.php new file mode 100644 index 0000000..d6a9c22 --- /dev/null +++ b/modules/gallery/views/admin_languages.html.php @@ -0,0 +1,118 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var old_default_locale = <?= html::js_string($default_locale) ?>; + + $("#g-languages-form").ready(function() { + $("input[name='installed_locales[]']").change(function (event) { + if (this.checked) { + $("input[type='radio'][value='" + this.value + "']").enable(); + } else { + if ($("input[type='radio'][value='" + this.value + "']").selected()) { // if you deselect your default language, switch to some other installed language + $("input[type='radio'][value='" + old_default_locale + "']").attr("checked", "checked"); + } + $("input[type='radio'][value='" + this.value + "']").attr("disabled", "disabled"); + } + }); + + $("#g-languages-form").ajaxForm({ + dataType: "json", + success: function(data) { + if (data.result == "success") { + el = $('<a href="' + <?= html::js_string(url::site("admin/maintenance/start/gallery_task::update_l10n?csrf=$csrf")) ?> + '"></a>'); // this is a little hack to trigger the update_l10n task in a dialog + el.gallery_dialog(); + el.trigger('click'); + } + } + }); + }); +</script> + +<div class="g-block"> + <h1> <?= t("Languages and translation") ?> </h1> + + <div class="g-block-content"> + + <div id="g-languages" class="g-block"> + <h2> <?= t("Languages") ?> </h2> + <p> + <?= t("Install new languages, update installed ones and set the default language for your Gallery.") ?> + </p> + + <div class="g-block-content ui-helper-clearfix"> + <form id="g-languages-form" method="post" action="<?= url::site("admin/languages/save") ?>"> + <?= access::csrf_form_field() ?> + <table class="g-left"> + <tr> + <th> <?= t("Installed") ?> </th> + <th> <?= t("Language") ?> </th> + <th> <?= t("Default language") ?> </th> + </tr> + <? $i = 0 ?> + <? foreach ($available_locales as $code => $display_name): ?> + <? if ($i == (int) (count($available_locales)/2)): ?> + </table> + <table class="g-left"> + <tr> + <th> <?= t("Installed") ?> </th> + <th> <?= t("Language") ?> </th> + <th> <?= t("Default language") ?> </th> + </tr> + <? endif ?> + <tr class="<?= (isset($installed_locales[$code])) ? "g-available" : "" ?><?= ($default_locale == $code) ? " g-selected" : "" ?>"> + <td> <?= form::checkbox("installed_locales[]", $code, isset($installed_locales[$code])) ?> </td> + <td> <?= $display_name ?> </td> + <td> + <?= form::radio("default_locale", $code, ($default_locale == $code), ((isset($installed_locales[$code]))?'':'disabled="disabled"') ) ?> + </td> + </tr> + <? $i++ ?> + <? endforeach ?> + </table> + <input type="submit" value="<?= t("Update languages")->for_html_attr() ?>" /> + </form> + </div> + </div> + + <div id="g-translations" class="g-block"> + <h2> <?= t("Translations") ?> </h2> + <p> + <?= t("Create your own translations and share them with the rest of the Gallery community.") ?> + </p> + + <div class="g-block-content"> + <a href="http://codex.galleryproject.org/Gallery3:Localization" target="_blank" + class="g-right ui-state-default ui-corner-all ui-icon ui-icon-help" + title="<?= t("Localization documentation")->for_html_attr() ?>"> + <?= t("Localization documentation") ?> + </a> + + <h3><?= t("Translating Gallery") ?></h3> + + <p><?= t("Follow these steps to begin translating Gallery.") ?></p> + + <ol> + <li><?= t("Make sure the target language is installed and up to date (check above).") ?></li> + <li><?= t("Make sure you have selected the right target language (currently %default_locale).", + array("default_locale" => locales::display_name())) ?></li> + <li><?= t("Start the translation mode and the translation interface will appear at the bottom of each Gallery page.") ?></li> + </ol> + <a href="<?= url::site("l10n_client/toggle_l10n_mode?csrf=".access::csrf_token()) ?>" + class="g-button ui-state-default ui-corner-all ui-icon-left"> + <span class="ui-icon ui-icon-power"></span> + <? if (Session::instance()->get("l10n_mode", false)): ?> + <?= t("Stop translation mode") ?> + <? else: ?> + <?= t("Start translation mode") ?> + <? endif ?> + </a> + + <h3><?= t("Sharing your translations") ?></h3> + <p> + <?= t("Sharing your own translations with the Gallery community is easy. Please do!") ?> + </p> + <?= $share_translations_form ?> + </div> + </div> + + </div> +</div> diff --git a/modules/gallery/views/admin_maintenance.html.php b/modules/gallery/views/admin_maintenance.html.php new file mode 100644 index 0000000..230e935 --- /dev/null +++ b/modules/gallery/views/admin_maintenance.html.php @@ -0,0 +1,212 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-admin-maintenance" class="g-block"> + <h1> <?= t("Maintenance") ?> </h1> + <div class="g-block-content"> + <div id="g-maintenance-mode"> + <p> + <?= t("When you're performing maintenance on your Gallery, you can enable <b>maintenance mode</b> which prevents any non-admin from accessing your Gallery. Some of the tasks below will automatically put your Gallery in maintenance mode for you.") ?> + </p> + <ul id="g-action-status" class="g-message-block"> + <? if (module::get_var("gallery", "maintenance_mode")): ?> + <li class="g-warning"> + <?= t("Maintenance mode is <b>on</b>. Non admins cannot access your Gallery. <a href=\"%enable_maintenance_mode_url\">Turn off maintenance mode</a>", array("enable_maintenance_mode_url" => url::site("admin/maintenance/maintenance_mode/0?csrf=$csrf"))) ?> + </li> + <? else: ?> + <li class="g-info"> + <?= t("Maintenance mode is off. User access is permitted. <a href=\"%enable_maintenance_mode_url\">Turn on maintenance mode</a>", array("enable_maintenance_mode_url" => url::site("admin/maintenance/maintenance_mode/1?csrf=$csrf"))) ?> + </li> + <? endif ?> + </ul> + </div> + </div> + + <div class="g-block-content"> + <div id="g-available-tasks"> + <h2> <?= t("Maintenance tasks") ?> </h2> + <p> + <?= t("Occasionally your Gallery will require some maintenance. Here are some tasks you can use to keep it running smoothly.") ?> + </p> + <table> + <tr> + <th> + <?= t("Name") ?> + </th> + <th> + <?= t("Description") ?> + </th> + <th> + <?= t("Action") ?> + </th> + </tr> + <? foreach ($task_definitions as $task): ?> + <tr class="<?= text::alternate("g-odd", "g-even") ?> <?= log::severity_class($task->severity) ?>"> + <td class="<?= log::severity_class($task->severity) ?>"> + <?= $task->name ?> + </td> + <td> + <?= $task->description ?> + </td> + <td> + <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"> + <?= t("run") ?> + </a> + </td> + </tr> + <? endforeach ?> + </table> + </div> + + <? if ($running_tasks->count()): ?> + <div id="g-running-tasks"> + <a href="<?= url::site("admin/maintenance/cancel_running_tasks?csrf=$csrf") ?>" + class="g-button g-right ui-icon-left ui-state-default ui-corner-all"> + <?= t("cancel all running") ?></a> + <h2> <?= t("Running tasks") ?> </h2> + <table> + <tr> + <th> + <?= t("Last updated") ?> + </th> + <th> + <?= t("Name") ?> + </th> + <th> + <?= t("Status") ?> + </th> + <th> + <?= t("Info") ?> + </th> + <th> + <?= t("Owner") ?> + </th> + <th> + <?= t("Action") ?> + </th> + </tr> + <? foreach ($running_tasks as $task): ?> + <tr class="<?= text::alternate("g-odd", "g-even") ?> <?= $task->state == "stalled" ? "g-warning" : "" ?>"> + <td class="<?= $task->state == "stalled" ? "g-warning" : "" ?>"> + <?= gallery::date_time($task->updated) ?> + </td> + <td> + <?= $task->name ?> + </td> + <td> + <? if ($task->done): ?> + <? if ($task->state == "cancelled"): ?> + <?= t("Cancelled") ?> + <? endif ?> + <?= t("Close") ?> + <? elseif ($task->state == "stalled"): ?> + <?= t("Stalled") ?> + <? else: ?> + <?= t("%percent_complete% Complete", array("percent_complete" => $task->percent_complete)) ?> + <? endif ?> + </td> + <td> + <?= $task->status ?> + </td> + <td> + <?= html::clean($task->owner()->name) ?> + </td> + <td> + <? if ($task->state == "stalled"): ?> + <a class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all" + href="<?= url::site("admin/maintenance/resume/$task->id?csrf=$csrf") ?>"> + <?= t("resume") ?> + </a> + <? endif ?> + <? if ($task->get_log()): ?> + <a href="<?= url::site("admin/maintenance/show_log/$task->id?csrf=$csrf") ?>" class="g-dialog-link g-button ui-state-default ui-corner-all"> + <?= t("view log") ?> + </a> + <? endif ?> + <a href="<?= url::site("admin/maintenance/cancel/$task->id?csrf=$csrf") ?>" + class="g-button ui-icon-left ui-state-default ui-corner-all"> + <?= t("cancel") ?> + </a> + </td> + </tr> + <? endforeach ?> + </table> + </div> + <? endif ?> + + <? if ($finished_tasks->count()): ?> + <div id="g-finished-tasks"> + <a href="<?= url::site("admin/maintenance/remove_finished_tasks?csrf=$csrf") ?>" + class="g-button g-right ui-icon-left ui-state-default ui-corner-all"> + <span class="ui-icon ui-icon-trash"></span><?= t("remove all finished") ?></a> + <h2> <?= t("Finished tasks") ?> </h2> + <table> + <tr> + <th> + <?= t("Last updated") ?> + </th> + <th> + <?= t("Name") ?> + </th> + <th> + <?= t("Status") ?> + </th> + <th> + <?= t("Info") ?> + </th> + <th> + <?= t("Owner") ?> + </th> + <th> + <?= t("Action") ?> + </th> + </tr> + <? foreach ($finished_tasks as $task): ?> + <tr class="<?= text::alternate("g-odd", "g-even") ?> <?= $task->state == "success" ? "g-success" : "g-error" ?>"> + <td class="<?= $task->state == "success" ? "g-success" : "g-error" ?>"> + <?= gallery::date_time($task->updated) ?> + </td> + <td> + <?= $task->name ?> + </td> + <td> + <? if ($task->state == "success"): ?> + <?= t("Success") ?> + <? elseif ($task->state == "error"): ?> + <?= t("Failed") ?> + <? elseif ($task->state == "cancelled"): ?> + <?= t("Cancelled") ?> + <? endif ?> + </td> + <td> + <?= $task->status ?> + </td> + <td> + <?= html::clean($task->owner()->name) ?> + </td> + <td> + <? if ($task->done): ?> + <a href="<?= url::site("admin/maintenance/remove/$task->id?csrf=$csrf") ?>" class="g-button ui-state-default ui-corner-all"> + <?= t("remove") ?> + </a> + <? if ($task->get_log()): ?> + <a href="<?= url::site("admin/maintenance/show_log/$task->id?csrf=$csrf") ?>" class="g-dialog-link g-button ui-state-default ui-corner-all"> + <?= t("view log") ?> + </a> + <? endif ?> + <? else: ?> + <a href="<?= url::site("admin/maintenance/resume/$task->id?csrf=$csrf") ?>" class="g-dialog-link g-button" ui-state-default ui-corner-all> + <?= t("resume") ?> + </a> + <a href="<?= url::site("admin/maintenance/cancel/$task->id?csrf=$csrf") ?>" class="g-button ui-state-default ui-corner-all"> + <?= t("cancel") ?> + </a> + <? endif ?> + </ul> + </td> + </tr> + <? endforeach ?> + </table> + </div> + <? endif ?> + </div> +</div> diff --git a/modules/gallery/views/admin_maintenance_show_log.html.php b/modules/gallery/views/admin_maintenance_show_log.html.php new file mode 100644 index 0000000..ecf882f --- /dev/null +++ b/modules/gallery/views/admin_maintenance_show_log.html.php @@ -0,0 +1,19 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + dismiss = function() { + window.location.reload(); + }; + download = function() { + // send request + $('<form action="<?= url::site("admin/maintenance/save_log/$task->id?csrf=$csrf") ?>" method="post"></form>'). +appendTo('body').submit().remove(); + }; +</script> +<div id="g-task-log-dialog"> + <h1> <?= $task->name ?> </h1> + <div class="g-task-log g-text-small"> + <pre><?= html::purify($task->get_log()) ?></pre> + </div> + <button id="g-close" class="ui-state-default ui-corner-all" onclick="dismiss()"><?= t("Close") ?></button> + <button id="g-save" class="ui-state-default ui-corner-all" onclick="download()"><?= t("Download") ?></button> +</div> diff --git a/modules/gallery/views/admin_maintenance_task.html.php b/modules/gallery/views/admin_maintenance_task.html.php new file mode 100644 index 0000000..013ac01 --- /dev/null +++ b/modules/gallery/views/admin_maintenance_task.html.php @@ -0,0 +1,84 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var target_value; + var animation = null; + var delta = 1; + var consecutive_error_count = 0; + animate_progress_bar = function() { + var current_value = parseInt($(".g-progress-bar div").css("width").replace("%", "")); + if (target_value > current_value) { + // speed up + delta = Math.min(delta + 0.04, 3); + } else { + // slow down + delta = Math.max(delta - 0.05, 1); + } + + if (target_value == 100) { + $(".g-progress-bar").progressbar("value", 100); + } else if (current_value != target_value || delta != 1) { + var new_value = Math.min(current_value + delta, target_value); + $(".g-progress-bar").progressbar("value", new_value); + animation = setTimeout(function() { animate_progress_bar(target_value); }, 100); + } else { + animation = null; + delta = 1; + } + $.fn.gallery_hover_init(); + } + + var FAILED_MSG = <?= t("Something went wrong...sorry! <a>Retry</a> or check the task log for details")->for_js() ?>; + var ERROR_MSG = <?= t("Something went wrong! Trying again in a moment... (__COUNT__)")->for_js() ?>; + update = function() { + $.ajax({ + url: <?= html::js_string(url::site("admin/maintenance/run/$task->id?csrf=$csrf")) ?>, + dataType: "json", + success: function(data) { + target_value = data.task.percent_complete; + consecutive_error_count = 0; + if (!animation) { + animate_progress_bar(); + } + $("#g-status").html("" + data.task.status); + if (data.task.done) { + $("#g-pause-button").hide(); + $("#g-done-button").show(); + } else { + setTimeout(update, 100); + } + }, + error: function(req, textStatus, errorThrown) { + if (textStatus == "timeout" || textStatus == "parsererror") { + consecutive_error_count++; + if (consecutive_error_count == 5) { + $("#g-status").html(FAILED_MSG); + $("#g-pause-button").hide(); + $("#g-done-button").show(); + consecutive_error_count = 0; // in case of a manual retry + $("#g-status a").attr("href", "javascript:update()"); + } else { + $("#g-status").html(ERROR_MSG.replace("__COUNT__", consecutive_error_count)); + // Give a little time to back off before retrying + setTimeout(update, 1500 * consecutive_error_count); + } + } + } + }); + } + $(".g-progress-bar").progressbar({value: 0}); + update(); + dismiss = function() { + window.location.reload(); + } +</script> +<div id="g-progress"> + <h1> <?= $task->name ?> </h1> + <div class="g-progress-bar"></div> + <div id="g-status"> + <?= t("Starting up...") ?> + </div> + <div class="g-text-right"> + <button id="g-pause-button" class="ui-state-default ui-corner-all" onclick="dismiss()"><?= t("Pause") ?></button> + <button id="g-done-button" class="ui-state-default ui-corner-all" style="display: none" onclick="dismiss()"><?= t("Close") ?></button> + </div> +</div> diff --git a/modules/gallery/views/admin_modules.html.php b/modules/gallery/views/admin_modules.html.php new file mode 100644 index 0000000..96576ae --- /dev/null +++ b/modules/gallery/views/admin_modules.html.php @@ -0,0 +1,129 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block ui-helper-clearfix"> + <script type="text/javascript"> + $("#g-module-update-form").ready(function() { + $("#g-module-update-form").ajaxForm({ + dataType: "json", + success: function(data) { + if (data.reload) { + window.location = "<? url::site("/admin/modules") ?>"; + } else { + $("body").append('<div id="g-dialog">' + data.dialog + '</div>'); + $("#g-dialog").dialog({ + bgiframe: true, + autoOpen: true, + autoResize: true, + modal: true, + resizable: false, + height: 400, + width: 500, + position: "center", + title: <?= t("Confirm module activation")->for_js() ?>, + buttons: { + <?= t("Continue")->for_js() ?>: function() { + $("form", this).submit(); + $(".ui-dialog-buttonpane button:contains(" + <?= t("Continue")->for_js() ?> + ")") + .attr("disabled", "disabled") + .addClass("ui-state-disabled"); + }, + <?= t("Cancel")->for_js() ?>: function() { + $(this).dialog("destroy").remove(); + } + } + }); + if (!data.allow_continue) { + $(".ui-dialog-buttonpane button:contains(" + <?= t("Continue")->for_js() ?> + ")") + .attr("disabled", "disabled") + .addClass("ui-state-disabled"); + } + } + } + }); + }); + </script> + <h1> <?= t("Gallery Modules") ?> </h1> + <p> + <?= t("Power up your Gallery by <a href=\"%url\">adding more modules</a>! Each module provides new cool features.", array("url" => "http://codex.galleryproject.org/Category:Gallery_3:Modules")) ?> + </p> + + <? if ($obsolete_modules_message): ?> + <p class="g-warning"> + <?= $obsolete_modules_message ?> + </p> + <? endif ?> + + <div class="g-block-content"> + <form id="g-module-update-form" method="post" action="<?= url::site("admin/modules/confirm") ?>"> + <?= access::csrf_form_field() ?> + <table> + <tr> + <th> <?= t("Installed") ?> </th> + <th style="width: 8em"> <?= t("Name") ?> </th> + <th> <?= t("Version") ?> </th> + <th> <?= t("Description") ?> </th> + <th style="width: 60px"> <?= t("Details") ?> </th> + </tr> + <? foreach ($available as $module_name => $module_info): ?> + <tr class="<?= text::alternate("g-odd", "g-even") ?>"> + <? $data = array("name" => $module_name); ?> + <? if ($module_info->locked) $data["disabled"] = 1; ?> + <td> <?= form::checkbox($data, '1', module::is_active($module_name)) ?> </td> + <td> <?= t($module_info->name) ?> </td> + <td> <?= $module_info->version ?> </td> + <td> <?= t($module_info->description) ?> </td> + <td style="white-space: nowrap"> + <ul class="g-buttonset"> + <li> + <a target="_blank" + <? if (isset($module_info->author_url)): ?> + class="ui-state-default ui-icon ui-icon-person ui-corner-left" + href="<?= $module_info->author_url ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-person ui-corner-left" + href="#" + <? endif ?> + + <? if (isset($module_info->author_name)): ?> + title="<?= $module_info->author_name ?>" + <? endif ?> + > + <? if (isset($module_info->author_name)): ?> + <?= $module_info->author_name ?> + <? endif ?> + </a> + </li> + <li> + <a target="_blank" + <? if (isset($module_info->info_url)): ?> + class="ui-state-default ui-icon ui-icon-info" + href="<?= $module_info->info_url ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-info" + href="#" + <? endif ?> + > + <?= t("info") ?> + </a> + </li> + <li> + <a target="_blank" + <? if (isset($module_info->discuss_url)): ?> + class="ui-state-default ui-icon ui-icon-comment ui-corner-right" + href="<?= $module_info->discuss_url ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-comment ui-corner-right" + href="#" + <? endif ?> + > + <?= t("discuss") ?> + </a> + </li> + </ul> + </td> + </tr> + <? endforeach ?> + </table> + <input type="submit" value="<?= t("Update")->for_html_attr() ?>" /> + </form> + </div> +</div> diff --git a/modules/gallery/views/admin_modules_confirm.html.php b/modules/gallery/views/admin_modules_confirm.html.php new file mode 100644 index 0000000..8c4cb2b --- /dev/null +++ b/modules/gallery/views/admin_modules_confirm.html.php @@ -0,0 +1,22 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="ui-helper-clearfix"> + <p> + <?= t("The following issue(s) have been identified:") ?> + </p> + + <div id="g-admin-modules-messages" class="g-block-content"> + <ul> + <? foreach (array("error" => "g-error", "warn" => "g-warning") as $type => $css_class): ?> + <? foreach ($messages[$type] as $message): ?> + <li class="<?= $css_class ?>" style="padding-bottom: 0"><?= $message ?></li> + <? endforeach ?> + <? endforeach ?> + </ul> + <form method="post" action="<?= url::site("admin/modules/save") ?>"> + <?= access::csrf_form_field() ?> + <? foreach ($modules as $module): ?> + <?= form::hidden($module, 1) ?> + <? endforeach ?> + </form> + </div> +</div> diff --git a/modules/gallery/views/admin_movies.html.php b/modules/gallery/views/admin_movies.html.php new file mode 100644 index 0000000..abf8fb2 --- /dev/null +++ b/modules/gallery/views/admin_movies.html.php @@ -0,0 +1,44 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-movies-admin" class="g-block ui-helper-clearfix"> + <h1> <?= t("Movies settings") ?> </h1> + <p> + <?= t("Gallery comes with everything it needs to upload and play movies.") ?> + <?= t("However, it needs the FFmpeg toolkit to extract thumbnails and size information from them.") ?> + </p> + <p> + <?= t("Although popular, FFmpeg is not installed on all Linux systems.") ?> + <?= t("To use FFmpeg without fully installing it, download a pre-compiled, <b>static build</b> of FFmpeg from one of the links <a href=\"%url\">here</a>.", array("url" => "http://ffmpeg.org/download.html")) ?> + <?= t("Then, put the \"ffmpeg\" file in Gallery's \"bin\" directory (e.g. \"/gallery3/bin\"), where Gallery will auto-detect it.") ?> + </p> + <p> + <?= t("Movies will work without FFmpeg, but their thumbnails will be placeholders.") ?> + </p> + <p> + <?= t("Can't get FFmpeg configured on your system? <a href=\"%url\">We can help!</a>", + array("url" => "http://codex.galleryproject.org/Gallery3:FAQ#Why_does_it_say_I.27m_missing_ffmpeg.3F")) ?> + </p> + + <div class="g-available"> + <h2> <?= t("Current configuration") ?> </h2> + <div id="g-ffmpeg" class="g-block"> + <img class="logo" width="284" height="70" src="<?= url::file("modules/gallery/images/ffmpeg.png") ?>" alt="<? t("Visit the FFmpeg project site") ?>" /> + <p> + <?= t("FFmpeg is a cross-platform standalone audio/video program.") ?><br/> + <?= t("Please refer to the <a href=\"%url\">FFmpeg website</a> for more information.", array("url" => "http://ffmpeg.org")) ?> + </p> + <div class="g-module-status g-info"> + <? if ($ffmpeg_dir): ?> + <? if ($ffmpeg_version): ?> + <p><?= t("FFmpeg version %version was found in %dir", array("version" => $ffmpeg_version, "dir" => $ffmpeg_dir)) ?></p> + <? else: ?> + <p><?= t("FFmpeg (of unknown version) was found in %dir", array("dir" => $ffmpeg_dir)) ?></p> + <? endif ?> + <? else: ?> + <p><?= t("We could not locate FFmpeg on your system.") ?></p> + <? endif ?> + </div> + </div> + </div> + + <?= $form ?> +</div> diff --git a/modules/gallery/views/admin_sidebar.html.php b/modules/gallery/views/admin_sidebar.html.php new file mode 100644 index 0000000..75499cb --- /dev/null +++ b/modules/gallery/views/admin_sidebar.html.php @@ -0,0 +1,64 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $(document).ready(function(){ + $(".g-admin-blocks-list").equal_heights(); + var extra_ht = $(".g-admin-blocks-list li").length * $(".g-admin-blocks-list li:first").height(); + $(".g-admin-blocks-list").height($(".g-admin-blocks-list").height() + extra_ht); + }); + + $(function() { + $(".g-admin-blocks-list ul").sortable({ + connectWith: ".g-sortable-blocks", + opacity: .7, + placeholder: "g-target", + update: function(event,ui) { + if ($(this).attr("id") == "g-active-blocks") { + var active_blocks = ""; + $("ul#g-active-blocks li").each(function(i) { + active_blocks += "&block["+i+"]="+$(this).attr("ref"); + }); + $.getJSON($("#g-site-blocks").attr("ref").replace("__ACTIVE__", active_blocks), function(data) { + if (data.result == "success") { + $("ul#g-available-blocks").html(data.available); + $("ul#g-active-blocks").html(data.active); + $("#g-action-status").remove(); + var message = "<ul id=\"g-action-status\" class=\"g-message-block\">"; + message += "<li class=\"g-success\">" + data.message + "</li>"; + message += "</ul>"; + $("#g-block-admin").before(message); + $("#g-action-status li").gallery_show_message(); + } + }); + } + } + }).disableSelection(); + }); +</script> + +<div id="g-block-admin" class="g-block ui-helper-clearfix"> + <h1> <?= t("Manage sidebar") ?> </h1> + <p> + <?= t("Select and drag blocks from the available column to the active column to add to the sidebar; remove by dragging the other way.") ?> + </p> + + <div class="g-block-content"> + <div id="g-site-blocks" ref="<?= url::site("admin/sidebar/update?csrf={$csrf}__ACTIVE__") ?>"> + <div class="g-admin-blocks-list g-left"> + <h3><?= t("Available blocks") ?></h3> + <div> + <ul id="g-available-blocks" class="g-sortable-blocks"> + <?= $available ?> + </ul> + </div> + </div> + <div class="g-admin-blocks-list g-left"> + <h3><?= t("Active blocks") ?></h3> + <div> + <ul id="g-active-blocks" class="g-sortable-blocks"> + <?= $active ?> + </ul> + </div> + </div> + </div> + </div> +</div> diff --git a/modules/gallery/views/admin_sidebar_blocks.html.php b/modules/gallery/views/admin_sidebar_blocks.html.php new file mode 100644 index 0000000..48aa3f0 --- /dev/null +++ b/modules/gallery/views/admin_sidebar_blocks.html.php @@ -0,0 +1,5 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> + +<? foreach ($blocks as $ref => $text): ?> +<li class="g-draggable" ref="<?= $ref ?>"><?= $text ?></li> +<? endforeach ?> diff --git a/modules/gallery/views/admin_theme_options.html.php b/modules/gallery/views/admin_theme_options.html.php new file mode 100644 index 0000000..e452913 --- /dev/null +++ b/modules/gallery/views/admin_theme_options.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block"> + <h1> <?= t("Theme options") ?> </h1> + <div class="g-block-content"> + <?= $form ?> + </div> +</div> diff --git a/modules/gallery/views/admin_themes.html.php b/modules/gallery/views/admin_themes.html.php new file mode 100644 index 0000000..547f27d --- /dev/null +++ b/modules/gallery/views/admin_themes.html.php @@ -0,0 +1,98 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var select_url = "<?= url::site("admin/themes/choose") ?>"; + select = function(type, id) { + $.post(select_url, {"type": type, "id": id, "csrf": '<?= $csrf ?>'}, + function() { load(type) }); + } +</script> + +<div class="g-block ui-helper-clearfix"> + <h1> <?= t("Theme choice") ?> </h1> + <p> + <?= t("Make your Gallery beautiful <a href=\"%url\">with a new theme</a>! There are separate themes for the regular site and for the administration interface. Click a theme below to preview and activate it.", array("url" => "http://codex.galleryproject.org/Category:Gallery_3:Themes")) ?> + </p> + + <div class="g-block-content"> + <div id="g-site-theme"> + <h2> <?= t("Gallery theme") ?> </h2> + <div class="g-block g-selected ui-helper-clearfix"> + <img src="<?= url::file("themes/{$site}/thumbnail.png") ?>" + alt="<?= html::clean_attribute($themes[$site]->name) ?>" /> + <h3> <?= $themes[$site]->name ?> </h3> + <p> + <?= $themes[$site]->description ?> + </p> + <? $v = new View("admin_themes_buttonset.html"); $v->info = $themes[$site]; print $v; ?> + </div> + + <h2> <?= t("Available Gallery themes") ?> </h2> + <div class="g-available"> + <? $count = 0 ?> + <? foreach ($themes as $id => $info): ?> + <? if (!$info->site) continue ?> + <? if ($id == $site) continue ?> + <div class="g-block ui-helper-clearfix"> + <a href="<?= url::site("admin/themes/preview/site/$id") ?>" class="g-dialog-link" title="<?= t("Theme Preview: %theme_name", array("theme_name" => $info->name))->for_html_attr() ?>"> + <img src="<?= url::file("themes/{$id}/thumbnail.png") ?>" + alt="<?= html::clean_attribute($info->name) ?>" /> + <h3> <?= $info->name ?> </h3> + <p> + <?= $info->description ?> + </p> + </a> + <? $v = new View("admin_themes_buttonset.html"); $v->info = $info; print $v; ?> + </div> + <? $count++ ?> + <? endforeach ?> + + <? if (!$count): ?> + <p> + <?= t("There are no other site themes available. <a href=\"%url\">Download one now!</a>", array("url" => "http://codex.galleryproject.org/Category:Gallery_3:Themes")) ?> + </p> + <? endif ?> + </div> + </div> + + <div id="g-admin-theme"> + <h2> <?= t("Admin theme") ?> </h2> + <div class="g-block g-selected ui-helper-clearfix"> + <img src="<?= url::file("themes/{$admin}/thumbnail.png") ?>" + alt="<?= html::clean_attribute($themes[$admin]->name) ?>" /> + <h3> <?= $themes[$admin]->name ?> </h3> + <p> + <?= $themes[$admin]->description ?> + </p> + <? $v = new View("admin_themes_buttonset.html"); $v->info = $themes[$admin]; print $v; ?> + </div> + + <h2> <?= t("Available admin themes") ?> </h2> + <div class="g-available"> + <? $count = 0 ?> + <? foreach ($themes as $id => $info): ?> + <? if (!$info->admin) continue ?> + <? if ($id == $admin) continue ?> + <div class="g-block ui-helper-clearfix"> + <a href="<?= url::site("admin/themes/preview/admin/$id") ?>" class="g-dialog-link" title="<?= t("Theme Preview: %theme_name", array("theme_name" => $info->name))->for_html_attr() ?>"> + <img src="<?= url::file("themes/{$id}/thumbnail.png") ?>" + alt="<?= html::clean_attribute($info->name) ?>" /> + <h3> <?= $info->name ?> </h3> + <p> + <?= $info->description ?> + </p> + </a> + <? $v = new View("admin_themes_buttonset.html"); $v->info = $info; print $v; ?> + </div> + <? $count++ ?> + <? endforeach ?> + + <? if (!$count): ?> + <p> + <?= t("There are no other admin themes available. <a href=\"%url\">Download one now!</a>", array("url" => "http://codex.galleryproject.org/Category:Gallery_3:Themes")) ?> + </p> + <? endif ?> + </div> + </div> + + </div> +</div> diff --git a/modules/gallery/views/admin_themes_buttonset.html.php b/modules/gallery/views/admin_themes_buttonset.html.php new file mode 100644 index 0000000..bf474a2 --- /dev/null +++ b/modules/gallery/views/admin_themes_buttonset.html.php @@ -0,0 +1,48 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul class="g-buttonset"> + <li> + <a target="_blank" + <? if (isset($info['author_url'])): ?> + class="ui-state-default ui-icon ui-icon-person ui-corner-left" + href="<?= $info['author_url'] ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-person ui-corner-left" + href="#" + <? endif ?> + + <? if (isset($info['author_name'])): ?> + title="<?= $info['author_name'] ?>" + <? endif ?> + > + <? if (isset($info['author_name'])): ?> + <?= $info['author_name'] ?> + <? endif ?> + </a> + </li> + <li> + <a target="_blank" + <? if (isset($info['info_url'])): ?> + class="ui-state-default ui-icon ui-icon-info" + href="<?= $info['info_url'] ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-info" + href="#" + <? endif ?> + > + <?= t("info") ?> + </a> + </li> + <li> + <a target="_blank" + <? if (isset($info['discuss_url'])): ?> + class="ui-state-default ui-icon ui-icon-comment ui-corner-right" + href="<?= $info['discuss_url'] ?>" + <? else: ?> + class="ui-state-disabled ui-icon ui-icon-comment ui-corner-right" + href="#" + <? endif ?> + > + <?= t("discuss") ?> + </a> + </li> +</ul> diff --git a/modules/gallery/views/admin_themes_preview.html.php b/modules/gallery/views/admin_themes_preview.html.php new file mode 100644 index 0000000..80a6158 --- /dev/null +++ b/modules/gallery/views/admin_themes_preview.html.php @@ -0,0 +1,8 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<h1><?= t("Preview of the %theme_name theme", array("theme_name" => $info->name)) ?></h1> +<p> + <a href="<?= url::site("admin/themes/choose/$type/$theme_name?csrf=$csrf") ?>"> + <?= t("Activate <strong>%theme_name</strong>", array("theme_name" => $info->name)) ?> + </a> +</p> +<iframe src="<?= $url ?>" style="width: 900px; height: 450px"></iframe> diff --git a/modules/gallery/views/error.html.php b/modules/gallery/views/error.html.php new file mode 100644 index 0000000..5d81b65 --- /dev/null +++ b/modules/gallery/views/error.html.php @@ -0,0 +1,12 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-error"> + <h1> + <?= t("Dang... Something went wrong!") ?> + </h1> + <h2> + <?= t("We tried really hard, but it's broken.") ?> + </h2> + <p> + <?= t("Talk to your Gallery administrator for help fixing this!") ?> + </p> +</div>
\ No newline at end of file diff --git a/modules/gallery/views/error_404.html.php b/modules/gallery/views/error_404.html.php new file mode 100644 index 0000000..42f62b6 --- /dev/null +++ b/modules/gallery/views/error_404.html.php @@ -0,0 +1,26 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-error"> + <h1> + <?= t("Dang... Page not found!") ?> + </h1> + <? if ($is_guest): ?> + <h2> + <?= t("Hey wait, you're not signed in yet!") ?> + </h2> + <p> + <?= t("Maybe the page exists, but is only visible to authorized users.") ?> + <?= t("Please sign in to find out.") ?> + </p> + <?= $login_form ?> + <script type="text/javascript"> + $(document).ready(function() { + $("#g-username").focus(); + }); + </script> + <? else: ?> + <p> + <?= t("Maybe the page exists, but is only visible to authorized users.") ?> + <?= t("If you think this is an error, talk to your Gallery administrator!") ?> + </p> + <? endif; ?> +</div> diff --git a/modules/gallery/views/error_admin.html.php b/modules/gallery/views/error_admin.html.php new file mode 100644 index 0000000..036e204 --- /dev/null +++ b/modules/gallery/views/error_admin.html.php @@ -0,0 +1,307 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? $error_id = uniqid("error") ?> +<? if (!function_exists("t")) { function t($msg) { return $msg; } } ?> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <style type="text/css"> + body { + background: #fff; + font-size: 14px; + line-height: 130%; + } + + div.big_box { + padding: 10px; + background: #eee; + border: solid 1px #ccc; + font-family: sans-serif; + color: #111; + width: 60em; + margin: 20px auto; + } + + #framework_error { + height: 6em; + } + + #framework_error .crashlogo { + position: relative; + top: .3em; + font-size: 6.0em; + } + + #framework_error .title { + position: relative; + top: -2.5em; + padding: 0px; + text-align: center; + } + + div#error_details { + text-align: left; + } + + code { + font-family: monospace; + font-size: 12px; + margin: 20px 20px 20px 0px; + color: #333; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + word-wrap: break-word; + } + + code .line { + padding-left: 10px; + } + + h3 { + font-family: sans-serif; + margin: 2px 0px 0px 0px; + padding: 8px 0px 0px 0px; + border-top: 1px solid #ddd; + } + + p { + padding: 0px; + margin: 0px 0px 10px 0px; + } + + li, pre { + padding: 0px; + margin: 0px; + } + + .collapsed { + display: none; + } + + .highlight { + font-weight: bold; + color: darkred; + } + + #kohana_error .message { + display: block; + padding-bottom: 10px; + } + + .source { + border: solid 1px #ccc; + background: #efe; + margin-bottom: 5px; + } + + table { + width: 100%; + display: block; + margin: 0 0 0.4em; + padding: 0; + border-collapse: collapse; + background: #efe; + } + + table td { + border: solid 1px #ddd; + text-align: left; + vertical-align: top; + padding: 0.4em; + } + + .args table td.key { + width: 200px; + } + + .number { + padding-right: 1em; + } + + #g-platform h2, #g-stats h2 { + font-size: 1.1em; + } + </style> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title><?= t("Something went wrong!") ?></title> + + <script type="text/javascript"> + function koggle(elem) { + elem = document.getElementById(elem); + if (elem.style && elem.style["display"]) { + // Only works with the "style" attr + var disp = elem.style["display"]; + } else { + if (elem.currentStyle) { + // For MSIE, naturally + var disp = elem.currentStyle["display"]; + } else { + if (window.getComputedStyle) { + // For most other browsers + var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display'); + } + } + } + + // Toggle the state of the "display" style + elem.style.display = disp == 'block' ? 'none' : 'block'; + return false; + } + </script> + </head> + <body> + <? try { $user = identity::active_user(); } catch (Exception $e) { } ?> + <div class="big_box" id="framework_error"> + <div class="crashlogo"> + :-( + </div> + <div class="title"> + <h1> + <?= t("Dang... Something went wrong!") ?> + </h1> + <h2> + <?= t("We tried really hard, but it's broken.") ?> + </h2> + </div> + </div> + <div class="big_box" id="error_details"> + <h2> + <?= t("Hey wait, you're an admin! We can tell you stuff.") ?> + </h2> + <p> + There's an error message below and you can find more details + in gallery3/var/logs (look for the file with the most recent + date on it). Stuck? Stop by the <a href="http://galleryproject.org/forum/96">Gallery 3 + Forums</a> and ask for help. You can also look at our list + of <a href="http://sourceforge.net/apps/trac/gallery/roadmap">open + tickets</a> to see if the problem you're seeing has been + reported. If you post a request, here's some useful + information to include: + <?= @gallery_block::get("platform_info") ?> + <?= @gallery_block::get("stats") ?> + </p> + <div id="kohana_error"> + <h3> + <span class="type"> + <?= $type?> [ <?= $code ?> ]: + </span> + <span class="message"> + <?= html::purify($message) ?> + </span> + </h3> + <div id="<?= $error_id ?>" class="content"> + <ol class="trace"> + <li class="snippet"> + <p> + <span class="file"> + <?= Kohana_Exception::debug_path($file)?>[ <?= $line?> ] + </span> + </p> + + <div class="source"> + <? if (Kohana_Exception::$source_output and $source_code = Kohana_Exception::debug_source($file, $line)): ?><code><? foreach ($source_code as $num => $row): ?><span class="line <?= ($num == $line) ? "highlight" : ""?>"><span class="number"><?= $num ?></span><?= htmlspecialchars($row, ENT_NOQUOTES, Kohana::CHARSET) ?></span><? endforeach ?></code> + <? endif ?> + </div> + </li> + + <? if (Kohana_Exception::$trace_output): ?> + <? foreach (Kohana_Exception::trace($trace) as $i => $step): ?> + <li class="snippet"> + <p> + <span class="file"> + <? if ($step["file"]): $source_id = "$error_id.source.$i" ?> + <? if (Kohana_Exception::$source_output and $step["source"]): ?> + <a href="#<?= $source_id ?>" onclick="return koggle('<?= $source_id ?>')"><?= Kohana_Exception::debug_path($step["file"])?>[ <?= $step["line"]?> ]</a> + <? else: ?> + <span class="file"><?= Kohana_Exception::debug_path($step["file"])?>[ <?= $step["line"]?> ]</span> + <? endif ?> + <? else: ?> + {<?= t("PHP internal call")?>} + <? endif?> + </span> + » + <?= $step["function"]?>(<? if ($step["args"]): $args_id = "$error_id.args.$i" ?> + <a href="#<?= $args_id ?>" onclick="return koggle('<?= $args_id ?>')"><?= t("arguments")?></a> + <? endif?>) + </p> + <? if (isset($args_id)): ?> + <div id="<?= $args_id ?>" class="args collapsed"> + <table cellspacing="0"> + <? foreach ($step["args"] as $name => $arg): ?> + <tr> + <td class="key"> + <pre><?= $name?></pre> + </td> + <td class="value"> + <pre><?= Kohana_Exception::safe_dump($arg, $name) ?></pre> + </td> + </tr> + <? endforeach?> + </table> + </div> + <? endif?> + <? if (Kohana_Exception::$source_output and $step["source"] and isset($source_id)): ?> + <pre id="<?= $source_id ?>" class="source collapsed"><code><? foreach ($step["source"] as $num => $row): ?><span class="line <?= ($num == $step["line"]) ? "highlight" : "" ?>"><span class="number"><?= $num ?></span><?= htmlspecialchars($row, ENT_NOQUOTES, Kohana::CHARSET) ?></span><? endforeach ?></code></pre> + <? endif?> + </li> + <? unset($args_id, $source_id) ?> + <? endforeach?> + </ol> + <? endif ?> + + </div> + <h2> + <a href="#<?= $env_id = $error_id."environment" ?>" onclick="return koggle('<?= $env_id ?>')"><?= t("Environment")?></a> + </h2> + <div id="<?= $env_id ?>" class="content collapsed"> + <? $included = get_included_files()?> + <h3><a href="#<?= $env_id = $error_id."environment_included" ?>" onclick="return koggle('<?= $env_id ?>')"><?= t("Included files")?></a>(<?= count($included)?>)</h3> + <div id="<?= $env_id ?>" class="collapsed"> + <table cellspacing="0"> + <? foreach ($included as $file): ?> + <tr> + <td> + <pre><?= Kohana_Exception::debug_path($file)?></pre> + </td> + </tr> + <? endforeach?> + </table> + </div> + <? $included = get_loaded_extensions()?> + <h3><a href="#<?= $env_id = $error_id."environment_loaded" ?>" onclick="return koggle('<?= $env_id ?>')"><?= t("Loaded extensions")?></a>(<?= count($included)?>)</h3> + <div id="<?= $env_id ?>" class="collapsed"> + <table cellspacing="0"> + <? foreach ($included as $file): ?> + <tr> + <td> + <pre><?= Kohana_Exception::debug_path($file)?></pre> + </td> + </tr> + <? endforeach?> + </table> + </div> + <? foreach (array("_SESSION", "_GET", "_POST", "_FILES", "_COOKIE", "_SERVER") as $var): ?> + <? if ( empty($GLOBALS[$var]) OR ! is_array($GLOBALS[$var])) continue ?> + <h3><a href="#<?= $env_id = "$error_id.environment" . strtolower($var) ?>" + onclick="return koggle('<?= $env_id ?>')">$<?= $var?></a></h3> + <div id="<?= $env_id ?>" class="collapsed"> + <table cellspacing="0"> + <? foreach ($GLOBALS[$var] as $key => $value): ?> + <tr> + <td class="key"> + <code> + <?= html::purify($key) ?> + </code> + </td> + <td class="value"> + <pre><?= Kohana_Exception::safe_dump($value, $key) ?></pre> + </td> + </tr> + <? endforeach?> + </table> + </div> + <? endforeach?> + </div> + </div> + </div> + </body> +</html> diff --git a/modules/gallery/views/error_cli.txt.php b/modules/gallery/views/error_cli.txt.php new file mode 100644 index 0000000..9f476f5 --- /dev/null +++ b/modules/gallery/views/error_cli.txt.php @@ -0,0 +1,3 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? echo Kohana_Exception::text($e), "\n"; + diff --git a/modules/gallery/views/error_user.html.php b/modules/gallery/views/error_user.html.php new file mode 100644 index 0000000..09ab752 --- /dev/null +++ b/modules/gallery/views/error_user.html.php @@ -0,0 +1,60 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? if (!function_exists("t")) { function t($msg) { return $msg; } } ?> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <style type="text/css"> + body { + background: #fff; + font-size: 14px; + line-height: 130%; + } + + div.big_box { + padding: 10px; + background: #eee; + border: solid 1px #ccc; + font-family: sans-serif; + color: #111; + width: 60em; + margin: 20px auto; + } + + #framework_error { + height: 8em; + } + + #framework_error .crashlogo { + position: relative; + top: .3em; + font-size: 6em; + } + + #framework_error .title { + position: relative; + top: -3em; + text-align: center; + margin: 0 auto; + } + </style> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title><?= t("Something went wrong!") ?></title> + </head> + <body> + <div class="big_box" id="framework_error"> + <div class="crashlogo"> + :-( + </div> + <div class="title"> + <h1> + <?= t("Dang... Something went wrong!") ?> + </h1> + <h2> + <?= t("We tried really hard, but it's broken.") ?> + </h2> + <p> + <?= t("Talk to your Gallery administrator for help fixing this!") ?> + </p> + </div> + </div> + </body> +</html> diff --git a/modules/gallery/views/form.html.php b/modules/gallery/views/form.html.php new file mode 100644 index 0000000..abc3216 --- /dev/null +++ b/modules/gallery/views/form.html.php @@ -0,0 +1,77 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? +print($open); + +// Not sure what to do with these, but at least show that we received them. +if ($class) { + print "<!-- unused class in form.html.php: $class -->"; +} +if ($title) { + print $title; +} + +if (!function_exists("DrawForm")) { + function DrawForm($inputs, $level=1) { + $error_messages = array(); + $prefix = str_repeat(" ", $level); + $haveGroup = false; + // On the first level, make sure we have a group if not add the <ul> tag now + if ($level == 1) { + foreach ($inputs as $input) { + $haveGroup |= $input->type == 'group'; + } + if (!$haveGroup) { + print "$prefix<ul>\n"; + } + } + + foreach ($inputs as $input) { + if ($input->type == 'group') { + print "$prefix<fieldset>\n"; + print "$prefix <legend>{$input->label}</legend>\n"; + print "$prefix <ul>\n"; + + DrawForm($input->inputs, $level + 2); + print "$prefix </ul>\n"; + + // Since hidden fields can only have name and value attributes lets just render it now + $hidden_prefix = "$prefix "; + foreach ($input->hidden as $hidden) { + print "$prefix {$hidden->render()}\n"; + } + print "$prefix</fieldset>\n"; + } else if ($input->type == 'script') { + print $input->render(); + } else { + if ($input->error_messages()) { + print "$prefix<li class=\"g-error\">\n"; + } else { + print "$prefix<li>\n"; + } + + if ($input->label()) { + print "$prefix {$input->label()}\n"; + } + print "$prefix {$input->render()}\n"; + if ($input->message()) { + print "$prefix <p>{$input->message()}</p>\n"; + } + if ($input->error_messages()) { + foreach ($input->error_messages() as $error_message) { + print "$prefix <p class=\"g-message g-error\">\n"; + print "$prefix $error_message\n"; + print "$prefix </p>\n"; + } + } + print "$prefix</li>\n"; + } + } + if ($level == 1 && !$haveGroup) { + print "$prefix</ul>\n"; + } + } +} +DrawForm($inputs); + +print($close); +?> diff --git a/modules/gallery/views/form_uploadify.html.php b/modules/gallery/views/form_uploadify.html.php new file mode 100644 index 0000000..bba6db7 --- /dev/null +++ b/modules/gallery/views/form_uploadify.html.php @@ -0,0 +1,167 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript" src="<?= url::file("lib/swfobject.js") ?>"></script> +<script type="text/javascript" src="<?= url::file("lib/uploadify/jquery.uploadify.min.js") ?>"></script> +<script type="text/javascript"> + <? $flash_minimum_version = "9.0.24" ?> + var success_count = 0; + var error_count = 0; + var updating = 0; + $("#g-add-photos-canvas").ready(function () { + var update_status = function() { + if (updating) { + // poor man's mutex + setTimeout(function() { update_status(); }, 500); + } + updating = 1; + $.get("<?= url::site("uploader/status/_S/_E") ?>" + .replace("_S", success_count).replace("_E", error_count), + function(data) { + $("#g-add-photos-status-message").html(data); + updating = 0; + }); + }; + + if (swfobject.hasFlashPlayerVersion("<?= $flash_minimum_version ?>")) { + $("#g-uploadify").uploadify({ + width: 298, + height: 32, + uploader: "<?= url::file("lib/uploadify/uploadify.swf.php") ?>", + script: "<?= url::site("uploader/add_photo/{$album->id}") ?>", + scriptData: <?= json_encode($script_data) ?>, + fileExt: "<?= implode(";", $extensions) ?>", + fileDesc: <?= t("Photos and movies")->for_js() ?>, + cancelImg: "<?= url::file("lib/uploadify/cancel.png") ?>", + simUploadLimit: <?= $simultaneous_upload_limit ?>, + sizeLimit: <?= $size_limit_bytes ?>, + wmode: "transparent", + hideButton: true, /* should be true */ + auto: true, + multi: true, + onAllComplete: function(filesUploaded, errors, allbytesLoaded, speed) { + $("#g-upload-cancel-all") + .addClass("ui-state-disabled") + .attr("disabled", "disabled"); + $("#g-upload-done") + .removeClass("ui-state-disabled") + .attr("disabled", null); + return true; + }, + onClearQueue: function(event) { + $("#g-upload-cancel-all") + .addClass("ui-state-disabled") + .attr("disabled", "disabled"); + $("#g-upload-done") + .removeClass("ui-state-disabled") + .attr("disabled", null); + return true; + }, + onComplete: function(event, queueID, fileObj, response, data) { + var re = /^error: (.*)$/i; + var msg = re.exec(response); + $("#g-add-photos-status ul").append( + "<li id=\"q" + queueID + "\" class=\"g-success\"><span></span> - " + + <?= t("Completed")->for_js() ?> + "</li>"); + $("#g-add-photos-status li#q" + queueID + " span").text(fileObj.name); + setTimeout(function() { $("#q" + queueID).slideUp("slow").remove() }, 5000); + success_count++; + update_status(); + return true; + }, + onError: function(event, queueID, fileObj, errorObj) { + if (errorObj.type == "HTTP") { + if (errorObj.info == "500") { + error_msg = <?= t("Unable to process this photo")->for_js() ?>; + } else if (errorObj.info == "404") { + error_msg = <?= t("The upload script was not found")->for_js() ?>; + } else if (errorObj.info == "400") { + error_msg = <?= t("This photo is too large (max is %size bytes)", + array("size" => $size_limit))->for_js() ?>; + } else { + msg += (<?= t("Server error: __INFO__ (__TYPE__)")->for_js() ?> + .replace("__INFO__", errorObj.info) + .replace("__TYPE__", errorObj.type)); + } + } else if (errorObj.type == "File Size") { + error_msg = <?= t("This photo is too large (max is %size bytes)", + array("size" => $size_limit))->for_js() ?>; + } else { + error_msg = <?= t("Server error: __INFO__ (__TYPE__)")->for_js() ?> + .replace("__INFO__", errorObj.info) + .replace("__TYPE__", errorObj.type); + } + msg = " - <a target=\"_blank\" href=\"http://codex.galleryproject.org/Gallery3:Troubleshooting:Uploading\">" + + error_msg + "</a>"; + + $("#g-add-photos-status ul").append( + "<li id=\"q" + queueID + "\" class=\"g-error\"><span></span>" + msg + "</li>"); + $("#g-add-photos-status li#q" + queueID + " span").text(fileObj.name); + $("#g-uploadify").uploadifyCancel(queueID); + error_count++; + update_status(); + }, + onSelect: function(event) { + if ($("#g-upload-cancel-all").hasClass("ui-state-disabled")) { + $("#g-upload-cancel-all") + .removeClass("ui-state-disabled") + .attr("disabled", null); + $("#g-upload-done") + .addClass("ui-state-disabled") + .attr("disabled", "disabled"); + } + return true; + } + }); + } else { + $(".requires-flash").hide(); + $(".no-flash").show(); + } + }); +</script> + +<div class="requires-flash"> + <? if ($suhosin_session_encrypt || (identity::active_user()->admin && !$movies_allowed)): ?> + <div class="g-message-block g-info"> + <? if ($suhosin_session_encrypt): ?> + <p class="g-error"> + <?= t("Error: your server is configured to use the <a href=\"%encrypt_url\"><code>suhosin.session.encrypt</code></a> setting from <a href=\"%suhosin_url\">Suhosin</a>. You must disable this setting to upload photos.", + array("encrypt_url" => "http://www.hardened-php.net/suhosin/configuration.html#suhosin.session.encrypt", + "suhosin_url" => "http://www.hardened-php.net/suhosin/")) ?> + </p> + <? endif ?> + + <? if (identity::active_user()->admin && !$movies_allowed): ?> + <p class="g-warning"> + <?= t("Movie uploading is disabled on your system. <a href=\"%help_url\">Help!</a>", array("help_url" => url::site("admin/movies"))) ?> + </p> + <? endif ?> + </div> + <? endif ?> + + <div> + <ul class="g-breadcrumbs"> + <? foreach ($album->parents() as $i => $parent): ?> + <li<? if ($i == 0) print " class=\"g-first\"" ?>> <?= html::clean($parent->title) ?> </li> + <? endforeach ?> + <li class="g-active"> <?= html::purify($album->title) ?> </li> + </ul> + </div> + + <div id="g-add-photos-canvas"> + <button id="g-add-photos-button" class="g-button ui-state-default ui-corner-all" href="#"><?= t("Select photos (%size max per file)...", array("size" => $size_limit)) ?></button> + <span id="g-uploadify"></span> + </div> + <div id="g-add-photos-status"> + <ul id="g-action-status" class="g-message-block"> + </ul> + </div> +</div> + +<div class="no-flash" style="display: none"> + <p> + <?= t("Your browser must have Adobe Flash Player version %flash_minimum_version or greater installed to use this feature.", array("flash_minimum_version" => $flash_minimum_version)) ?> + </p> + <a href="http://www.adobe.com/go/getflashplayer"> + <img src="<?= request::protocol() ?>://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" + alt=<?= t("Get Adobe Flash Player")->for_js() ?> /> + </a> +</div> diff --git a/modules/gallery/views/form_uploadify_buttons.html.php b/modules/gallery/views/form_uploadify_buttons.html.php new file mode 100644 index 0000000..e002d82 --- /dev/null +++ b/modules/gallery/views/form_uploadify_buttons.html.php @@ -0,0 +1,11 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="requires-flash"> + <!-- Proxy the done request back to our form, since its been ajaxified --> + <button id="g-upload-done" class="ui-state-default ui-corner-all" onclick="$('#g-add-photos-form').submit();return false;"> + <?= t("Done") ?> + </button> + <button id="g-upload-cancel-all" class="ui-state-default ui-corner-all ui-state-disabled" onclick="$('#g-uploadify').uploadifyClearQueue();return false;" disabled="disabled"> + <?= t("Cancel uploads") ?> + </button> + <span id="g-add-photos-status-message" /> +</div> diff --git a/modules/gallery/views/in_place_edit.html.php b/modules/gallery/views/in_place_edit.html.php new file mode 100644 index 0000000..2d6cbe9 --- /dev/null +++ b/modules/gallery/views/in_place_edit.html.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= form::open($action, array("method" => "post", "id" => "g-in-place-edit-form", "class" => "g-short-form")) ?> + <?= access::csrf_form_field() ?> + <ul> + <li<? if (!empty($errors["input"])): ?> class="g-error"<? endif ?>> + <?= form::input("input", $form["input"], " class=\"textbox\"") ?> + </li> + <li> + <?= form::submit(array("class" => "submit ui-state-default"), t("Save")) ?> + </li> + <li><a href="#" class="g-cancel"><?= t("Cancel") ?></a></li> + <? if (!empty($errors["input"])): ?> + <li> + <p id="g-in-place-edit-message" class="g-error"><?= $errors["input"] ?></p> + </li> + <? endif ?> + </ul> +</form> + + + diff --git a/modules/gallery/views/kohana/error.php b/modules/gallery/views/kohana/error.php new file mode 100644 index 0000000..0e84f09 --- /dev/null +++ b/modules/gallery/views/kohana/error.php @@ -0,0 +1,46 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? +// This is the template for all HTML errors. If you're throwing an exception and you want your +// error to appear differently, extend Kohana_Exception and specify a different template. + +// Log validation exceptions to ease debugging +if ($e instanceof ORM_Validation_Exception) { + Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1)); +} + +if (php_sapi_name() == "cli") { + include Kohana::find_file("views", "error_cli.txt"); + return; +} + +try { + // Admins get a special error page + $user = identity::active_user(); + if ($user && $user->admin) { + include Kohana::find_file("views", "error_admin.html"); + return; + } +} catch (Exception $ignored) { +} + +// Try to show a themed error page for 404 errors +if ($e instanceof Kohana_404_Exception) { + if (Router::$controller == "file_proxy") { + print "File not found"; + } else { + $view = new Theme_View("page.html", "other", "error"); + $view->page_title = t("Dang... Page not found!"); + $view->content = new View("error_404.html"); + $user = identity::active_user(); + $view->content->is_guest = $user && $user->guest; + if ($view->content->is_guest) { + $view->content->login_form = new View("login_ajax.html"); + $view->content->login_form->form = auth::get_login_form("login/auth_html"); + } + print $view; + } + return; +} + +header("HTTP/1.1 500 Internal Server Error"); +include Kohana::find_file("views", "error_user.html"); diff --git a/modules/gallery/views/kohana_profiler.php b/modules/gallery/views/kohana_profiler.php new file mode 100644 index 0000000..c753434 --- /dev/null +++ b/modules/gallery/views/kohana_profiler.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<style type="text/css"> + #kohana-profiler { + background-color: #F8FFF8; + border: 1px solid #E5EFF8; + clear: both; + font-family: Monaco, 'Courier New'; + margin-top: 20px; + padding: 10px 10px 0; + text-align: left; + } + #kohana-profiler pre { + font: inherit; + margin: 0; + } + #kohana-profiler .kp-meta { + background: #fff; + border: 1px solid #E5EFF8; + color: #A6B0B8; + margin: 0 0 10px; + padding: 4px; + text-align: center; + } + #kohana-profiler td { + padding-right: 1em; + } + <? echo $styles ?> +</style> + +<div id="kohana-profiler"> + <? foreach ($profiles as $profile): ?> + <?= $profile->render(); ?> + <? endforeach; ?> + <p class="kp-meta"><?= t("Profiler executed in ") . number_format($execution_time, 3) ?>s</p> +</div> diff --git a/modules/gallery/views/l10n_client.html.php b/modules/gallery/views/l10n_client.html.php new file mode 100644 index 0000000..47a45e2 --- /dev/null +++ b/modules/gallery/views/l10n_client.html.php @@ -0,0 +1,82 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="l10n-client" class="hidden"> + <div class="labels"> + <span id="l10n-client-toggler"> + <a id="g-minimize-l10n">_</a> + <a id="g-close-l10n" title="<?= t("Stop the translation mode")->for_html_attr() ?>" + href="<?= html::clean_attribute(url::site("l10n_client/toggle_l10n_mode?csrf=".access::csrf_token())) ?>">X</a> + </span> + <div class="label strings"><h2><?= t("Page text") ?> + <? if (!Input::instance()->get('show_all_l10n_messages')): ?> + <a style="background-color:#fff" href="<?= url::site("admin/languages?show_all_l10n_messages=1") ?>"><?= t("(Show all)") ?></a> + <? endif; ?> + </h2></div> + <div class="label source"><h2><?= t("Source") ?></div> + <div class="label translation"><h2><?= t("Translation to %language", + array("language" => locales::display_name())) ?></h2></div> + </div> + <div id="l10n-client-string-select"> + <ul class="string-list"> + <? foreach ($string_list as $string): ?> + <li class="<?= $string["translation"] === "" ? "untranslated" : "translated" ?>"> + <? if (is_array($string["source"])): ?> + [one] - <?= $string["source"]["one"] ?><br/> + [other] - <?= $string["source"]["other"] ?> + <? else: ?> + <?= $string["source"] ?> + <? endif; ?> + </li> + <? endforeach; ?> + </ul> + + <?= $l10n_search_form ?> + </div> + <div id="l10n-client-string-editor"> + <div class="source"> + <p class="source-text"></p> + <p id="source-text-tmp-space" style="display:none"></p> + </div> + <div class="translation"> + <form method="post" action="<?= url::site("l10n_client/save") ?>" id="g-l10n-client-save-form"> + <?= access::csrf_form_field() ?> + <?= form::hidden("l10n-message-key") ?> + <?= form::textarea("l10n-edit-translation", "", ' id="l10n-edit-translation" rows="5" class="translationField"') ?> + <div id="plural-zero" class="translationField hidden"> + <label for="l10n-edit-plural-translation-zero">[zero]</label> + <?= form::textarea("l10n-edit-plural-translation-zero", "", ' rows="2"') ?> + </div> + <div id="plural-one" class="translationField hidden"> + <label for="l10n-edit-plural-translation-one">[one]</label> + <?= form::textarea("l10n-edit-plural-translation-one", "", ' rows="2"') ?> + </div> + <div id="plural-two" class="translationField hidden"> + <label for="l10n-edit-plural-translation-two">[two]</label> + <?= form::textarea("l10n-edit-plural-translation-two", "", ' rows="2"') ?> + </div> + <div id="plural-few" class="translationField hidden"> + <label for="l10n-edit-plural-translation-few">[few]</label> + <?= form::textarea("l10n-edit-plural-translation-few", "", ' rows="2"') ?> + </div> + <div id="plural-many" class="translationField hidden"> + <label for="l10n-edit-plural-translation-many">[many]</label> + <?= form::textarea("l10n-edit-plural-translation-many", "", ' rows="2"') ?> + </div> + <div id="plural-other" class="translationField hidden"> + <label for="l10n-edit-plural-translation-other">[other]</label> + (<a href="http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html"><?= t("learn more about plural forms") ?></a>) + <?= form::textarea("l10n-edit-plural-translation-other", "", ' rows="2"') ?> + </div> + <input type="submit" name="l10n-edit-save" value="<?= t("Save translation")->for_html_attr() ?>"/> + <a href="javascript: Gallery.l10nClient.copySourceText()" + class="g-button ui-state-default ui-corner-all"><?= t("Copy source text") ?></a> + </form> + </div> + </div> + <script type="text/javascript"> + var MSG_TRANSLATE_TEXT = <?= t("Translate text")->for_js() ?>; + var l10n_client_data = <?= json_encode($string_list) ?>; + var plural_forms = <?= json_encode($plural_forms) ?>; + var toggle_l10n_mode_url = <?= html::js_string(url::site("l10n_client/toggle_l10n_mode")) ?>; + var csrf = <?= html::js_string(access::csrf_token()) ?>; + </script> +</div> diff --git a/modules/gallery/views/login_ajax.html.php b/modules/gallery/views/login_ajax.html.php new file mode 100644 index 0000000..a40d195 --- /dev/null +++ b/modules/gallery/views/login_ajax.html.php @@ -0,0 +1,52 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $("#g-login-form").ready(function() { + $("#g-password-reset").click(function() { + $.ajax({ + url: "<?= url::site("password/reset") ?>", + success: function(data) { + $("#g-login").html(data); + $("#ui-dialog-title-g-dialog").html(<?= t("Reset password")->for_js() ?>); + $(".submit").addClass("g-button ui-state-default ui-corner-all"); + $(".submit").gallery_hover_init(); + ajaxify_login_reset_form(); + + // See comment about IE7 below + setTimeout('$("#g-name").focus()', 100); + } + }); + }); + + // Setting the focus here doesn't work on IE7, perhaps because the field is + // not ready yet? So set a timeout and do it the next time we're idle + setTimeout('$("#g-username").focus()', 100); + }); + + function ajaxify_login_reset_form() { + $("#g-login form").ajaxForm({ + dataType: "json", + success: function(data) { + if (data.form) { + $("#g-login form").replaceWith(data.form); + ajaxify_login_reset_form(); + } + if (data.result == "success") { + $("#g-dialog").dialog("close"); + window.location.reload(); + } + } + }); + }; +</script> +<div id="g-login"> + <ul> + <li id="g-login-form"> + <?= $form ?> + </li> + <? if (identity::is_writable() && !module::get_var("gallery", "maintenance_mode")): ?> + <li> + <a href="#" id="g-password-reset" class="g-right g-text-small"><?= t("Forgot your password?") ?></a> + </li> + <? endif ?> + </ul> +</div> diff --git a/modules/gallery/views/login_current_user.html.php b/modules/gallery/views/login_current_user.html.php new file mode 100644 index 0000000..9452557 --- /dev/null +++ b/modules/gallery/views/login_current_user.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li> + <? $name = $menu->label->for_html() ?> + <? $hover_text = t("Your profile")->for_html_attr() ?> + <?= t("Logged in as %name", array("name" => html::mark_clean( + "<a href='$menu->url' title='$hover_text' id='$menu->id'>{$name}</a>"))) ?> +</li> diff --git a/modules/gallery/views/menu.html.php b/modules/gallery/views/menu.html.php new file mode 100644 index 0000000..17a249d --- /dev/null +++ b/modules/gallery/views/menu.html.php @@ -0,0 +1,24 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? if (!$menu->is_empty()): // Don't show the menu if it has no choices ?> +<? if ($menu->is_root): ?> +<ul <?= $menu->css_id ? "id='$menu->css_id'" : "" ?> class="<?= $menu->css_class ?>"> + <? foreach ($menu->elements as $element): ?> + <?= $element->render() ?> + <? endforeach ?> +</ul> + +<? else: ?> + +<li title="<?= $menu->label->for_html_attr() ?>"> + <a href="#"> + <?= $menu->label->for_html() ?> + </a> + <ul> + <? foreach ($menu->elements as $element): ?> + <?= $element->render() ?> + <? endforeach ?> + </ul> +</li> + +<? endif ?> +<? endif ?> diff --git a/modules/gallery/views/menu_ajax_link.html.php b/modules/gallery/views/menu_ajax_link.html.php new file mode 100644 index 0000000..06cd6f9 --- /dev/null +++ b/modules/gallery/views/menu_ajax_link.html.php @@ -0,0 +1,10 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li> + <a <?= $menu->css_id ? "id='{$menu->css_id}'" : "" ?> + class="g-ajax-link <?= $menu->css_class ?>" + href="<?= $menu->url ?>" + title="<?= $menu->label->for_html_attr() ?>" + ajax_handler="<?= $menu->ajax_handler ?>"> + <?= $menu->label->for_html() ?> + </a> +</li> diff --git a/modules/gallery/views/menu_dialog.html.php b/modules/gallery/views/menu_dialog.html.php new file mode 100644 index 0000000..b44080c --- /dev/null +++ b/modules/gallery/views/menu_dialog.html.php @@ -0,0 +1,9 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li> + <a <?= $menu->css_id ? "id='{$menu->css_id}'" : "" ?> + class="g-dialog-link <?= $menu->css_class ?>" + href="<?= $menu->url ?>" + title="<?= $menu->label->for_html_attr() ?>"> + <?= $menu->label->for_html() ?> + </a> +</li> diff --git a/modules/gallery/views/menu_link.html.php b/modules/gallery/views/menu_link.html.php new file mode 100644 index 0000000..a36d275 --- /dev/null +++ b/modules/gallery/views/menu_link.html.php @@ -0,0 +1,9 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li> + <a <?= $menu->css_id ? "id='{$menu->css_id}'" : "" ?> + class="g-menu-link <?= $menu->css_class ?>" + href="<?= $menu->url ?>" + title="<?= $menu->label->for_html_attr() ?>"> + <?= $menu->label->for_html() ?> + </a> +</li> diff --git a/modules/gallery/views/movieplayer.html.php b/modules/gallery/views/movieplayer.html.php new file mode 100644 index 0000000..3cf5d17 --- /dev/null +++ b/modules/gallery/views/movieplayer.html.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= html::anchor($url, "", $attrs) ?> +<script type="text/javascript"> + var id = "<?= $attrs["id"] ?>"; + var max_size = <?= $max_size ?>; + // set the size of the movie html anchor, taking into account max_size and height of control bar + function set_movie_size(width, height) { + if((width > max_size) || (height > max_size)) { + if (width > height) { + height = Math.ceil(height * max_size / width); + width = max_size; + } else { + width = Math.ceil(width * max_size / height); + height = max_size; + } + } + height += flowplayer(id).getConfig().plugins.controls.height; + $("#" + id).css({width: width, height: height}); + }; + // setup flowplayer + flowplayer(id, + $.extend(true, { + "src": "<?= url::abs_file("lib/flowplayer.swf.php") ?>", + "wmode": "transparent", + "provider": "pseudostreaming" + }, <?= json_encode($fp_params) ?>), + $.extend(true, { + "plugins": { + "pseudostreaming": { + "url": "<?= url::abs_file("lib/flowplayer.pseudostreaming-byterange.swf.php") ?>" + }, + "controls": { + "autoHide": "always", + "hideDelay": 2000, + "height": 24, + "url": "<?= url::abs_file("lib/flowplayer.controls.swf.php") ?>" + } + }, + "clip": { + "scaling": "fit", + "onMetaData": function(clip) { + // set movie size a second time using actual size from metadata + set_movie_size(parseInt(clip.metaData.width), parseInt(clip.metaData.height)); + } + } + }, <?= json_encode($fp_config) ?>) + ).ipad(); + // set movie size using width and height passed from movie_img function + $("document").ready(set_movie_size(<?= $width ?>, <?= $height ?>)); +</script> diff --git a/modules/gallery/views/permissions_browse.html.php b/modules/gallery/views/permissions_browse.html.php new file mode 100644 index 0000000..0b27336 --- /dev/null +++ b/modules/gallery/views/permissions_browse.html.php @@ -0,0 +1,62 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var form_url = "<?= url::site("permissions/form/__ITEM__") ?>"; + show = function(id) { + $.ajax({ + url: form_url.replace("__ITEM__", id), + success: function(data) { + $("#g-edit-permissions-form").html(data); + $(".g-active").removeClass("g-active"); + $("#item-" + id).addClass("g-active"); + } + }); + } + + var action_url = + "<?= url::site("permissions/change/__CMD__/__GROUP__/__PERM__/__ITEM__?csrf=$csrf") ?>"; + set = function(cmd, group_id, perm_id, item_id) { + $.ajax({ + url: action_url.replace("__CMD__", cmd).replace("__GROUP__", group_id). + replace("__PERM__", perm_id).replace("__ITEM__", item_id), + success: function(data) { + $("#g-edit-permissions-form").load(form_url.replace("__ITEM__", item_id)); + } + }); + } +</script> +<div id="g-permissions"> + <? if (!$htaccess_works): ?> + <ul id="g-action-status" class="g-message-block"> + <li class="g-error"> + <?= t("Oh no! Your server needs a configuration change in order for you to hide photos! Ask your server administrator to enable <a %mod_rewrite_attrs>mod_rewrite</a> and set <a %apache_attrs><i>AllowOverride FileInfo Options</i></a> to fix this.", + array("mod_rewrite_attrs" => html::mark_clean('href="http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html" target="_blank"'), + "apache_attrs" => html::mark_clean('href="http://httpd.apache.org/docs/2.0/mod/core.html#allowoverride" target="_blank"'))) ?> + </li> + </ul> + <? endif ?> + + <p><?= t("Edit permissions for album:") ?></p> + + <ul class="g-breadcrumbs"> + <? $i = 0 ?> + <? foreach ($parents as $parent): ?> + <li id="item-<?= $parent->id ?>"<? if ($i == 0) print " class=\"g-first\"" ?>> + <? if (access::can("edit", $parent)): ?> + <a href="javascript:show(<?= $parent->id ?>)"> <?= html::purify($parent->title) ?> </a> + <? else: ?> + <?= html::purify($parent->title) ?> + <? endif ?> + </li> + <? $i++ ?> + <? endforeach ?> + <li class="g-active" id="item-<?= $item->id ?>"> + <a href="javascript:show(<?= $item->id ?>)"> + <?= html::purify($item->title) ?> + </a> + </li> + </ul> + + <div id="g-edit-permissions-form"> + <?= $form ?> + </div> +</div> diff --git a/modules/gallery/views/permissions_form.html.php b/modules/gallery/views/permissions_form.html.php new file mode 100644 index 0000000..f171411 --- /dev/null +++ b/modules/gallery/views/permissions_form.html.php @@ -0,0 +1,92 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<fieldset> + <legend> <?= t('Edit Permissions') ?> </legend> + <table> + <tr> + <th> </th> + <? foreach ($groups as $group): ?> + <th> <?= html::clean($group->name) ?> </th> + <? endforeach ?> + </tr> + + <? foreach ($permissions as $permission): ?> + <tr> + <td> <?= t($permission->display_name) ?> + </td> + <? foreach ($groups as $group): ?> + <? $intent = access::group_intent($group, $permission->name, $item) ?> + <? $allowed = access::group_can($group, $permission->name, $item) ?> + <? $lock = access::locked_by($group, $permission->name, $item) ?> + + <? if ($lock): ?> + <td class="g-denied"> + <img src="<?= url::file(gallery::find_file("images", "ico-denied.png")) ?>" + title="<?= t('denied and locked through parent album')->for_html_attr() ?>" + alt="<?= t('denied icon')->for_html_attr() ?>" /> + <a href="javascript:show(<?= $lock->id ?>)" title="<?= t('click to go to parent album')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-lock.png")) ?>" alt="<?= t('locked icon')->for_html_attr() ?>" /> + </a> + </td> + <? else: ?> + <? if ($intent === access::INHERIT): ?> + <? if ($allowed): ?> + <td class="g-allowed"> + <a href="javascript:set('allow',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" title="<?= t('allowed through parent album, click to allow explicitly')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-success-passive.png")) ?>" alt="<?= t('passive allowed icon')->for_html_attr() ?>" /> + </a> + <a href="javascript:set('deny',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('click to deny')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-denied-inactive.png")) ?>" alt="<?= t('inactive denied icon')->for_html_attr() ?>" /> + </a> + </td> + <? else: ?> + <td class="g-denied"> + <a href="javascript:set('allow',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('click to allow')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-success-inactive.png")) ?>" alt="<?= t('inactive allowed icon')->for_html_attr() ?>" /> + </a> + <a href="javascript:set('deny',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('denied through parent album, click to deny explicitly')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-denied-passive.png")) ?>" alt="<?= t('passive denied icon')->for_html_attr() ?>" /> + </a> + </td> + <? endif ?> + + <? elseif ($intent === access::DENY): ?> + <td class="g-denied"> + <a href="javascript:set('allow',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('click to allow')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-success-inactive.png")) ?>" alt="<?= t('inactive allowed icon')->for_html_attr() ?>" /> + </a> + <? if ($item->id == 1): ?> + <img src="<?= url::file(gallery::find_file("images", "ico-denied.png")) ?>" alt="<?= t('denied icon')->for_html_attr() ?>" title="<?= t('denied')->for_html_attr() ?>"/> + <? else: ?> + <a href="javascript:set('reset',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('denied, click to reset')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-denied.png")) ?>" alt="<?= t('denied icon')->for_html_attr() ?>" /> + </a> + <? endif ?> + </td> + <? elseif ($intent === access::ALLOW): ?> + <td class="g-allowed"> + <? if ($item->id == 1): ?> + <img src="<?= url::file(gallery::find_file("images", "ico-success.png")) ?>" title="<?= t("allowed")->for_html_attr() ?>" alt="<?= t('allowed icon')->for_html_attr() ?>" /> + <? else: ?> + <a href="javascript:set('reset',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('allowed, click to reset')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-success.png")) ?>" alt="<?= t('allowed icon')->for_html_attr() ?>" /> + </a> + <? endif ?> + <a href="javascript:set('deny',<?= $group->id ?>,<?= $permission->id ?>,<?= $item->id ?>)" + title="<?= t('click to deny')->for_html_attr() ?>"> + <img src="<?= url::file(gallery::find_file("images", "ico-denied-inactive.png")) ?>" alt="<?= t('inactive denied icon')->for_html_attr() ?>" /> + </a> + </td> + <? endif ?> + <? endif ?> + </td> + <? endforeach ?> + </tr> + <? endforeach ?> + </table> +</fieldset> diff --git a/modules/gallery/views/quick_delete_confirm.html.php b/modules/gallery/views/quick_delete_confirm.html.php new file mode 100644 index 0000000..176ffb9 --- /dev/null +++ b/modules/gallery/views/quick_delete_confirm.html.php @@ -0,0 +1,12 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="ui-helper-clearfix"> + <p> + <? if ($item->is_album()): ?> + <?= t("Delete the album <b>%title</b>? All photos and movies in the album will also be deleted.", + array("title" => html::purify($item->title))) ?> + <? else: ?> + <?= t("Are you sure you want to delete <b>%title</b>?", array("title" => html::purify($item->title))) ?> + <? endif ?> + </p> + <?= $form ?> +</div> diff --git a/modules/gallery/views/reauthenticate.html.php b/modules/gallery/views/reauthenticate.html.php new file mode 100644 index 0000000..9a6696f --- /dev/null +++ b/modules/gallery/views/reauthenticate.html.php @@ -0,0 +1,15 @@ +<?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 ?> + <script type="text/javascript"> + $("#g-reauthenticate-form").ready(function() { + $("#g-password").focus(); + }); + </script> +</div>
\ No newline at end of file diff --git a/modules/gallery/views/upgrade_checker_block.html.php b/modules/gallery/views/upgrade_checker_block.html.php new file mode 100644 index 0000000..c984d99 --- /dev/null +++ b/modules/gallery/views/upgrade_checker_block.html.php @@ -0,0 +1,55 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<p> + <?= t("Gallery can check to see if there is a new version available for you to use. It is a good idea to upgrade your Gallery to get the latest features and security fixes. Your privacy is important so no information about your Gallery is shared during this process. You can disable this feature below.") ?> +</p> + +<p> + <? if (gallery::RELEASE_CHANNEL == "release"): ?> + <?= t("You are using the official Gallery %version release, code named <i>%code_name</i>.", array("version" => gallery::VERSION, "code_name" => gallery::CODE_NAME)) ?> + <? elseif (isset($build_number)): ?> + <?= t("You are using an experimental snapshot of Gallery %version (build %build_number on branch %branch).", array("version" => gallery::VERSION, "branch" => gallery::RELEASE_BRANCH, "build_number" => $build_number)) ?> + <? else: ?> + <?= t("You are using an experimental snapshot of Gallery %version (branch %branch) but your gallery3/.build_number file is missing so we don't know what build you have. You should probably upgrade so that you have that file.", array("version" => gallery::VERSION, "branch" => gallery::RELEASE_BRANCH, "build_number" => $build_number)) ?> + <? endif ?> +</p> + +<? if ($new_version): ?> +<ul class="g-message-block"> + <li class="g-message g-info"> + <?= $new_version ?> + </li> +</ul> +<? endif ?> + +<p> + <a class="g-button ui-state-default ui-corner-all" + href="<?= url::site("admin/upgrade_checker/check_now?csrf=$csrf") ?>"> + <?= t("Check now") ?> + </a> + <? if ($auto_check_enabled): ?> + <a class="g-button ui-state-default ui-corner-all" + href="<?= url::site("admin/upgrade_checker/set_auto/0?csrf=$csrf") ?>"> + <?= t("Disable automatic checking") ?> + </a> + <? else: ?> + <a class="g-button ui-state-default ui-corner-all" + href="<?= url::site("admin/upgrade_checker/set_auto/1?csrf=$csrf") ?>"> + <?= t("Enable automatic checking") ?> + </a> + <? endif ?> +</p> + +<p class="g-text-small"> + <? if ($auto_check_enabled): ?> + <?= t("Automatic upgrade checking is enabled.") ?> + <? else: ?> + <?= t("Automatic upgrade checking is disabled.") ?> + <? endif ?> + <? if (!$version_info): ?> + <?= t("No upgrade checks have been made yet.") ?> + <? else: ?> + <?= t("The last upgrade check was made on %date.", + array("date" => gallery::date_time($version_info->timestamp))) ?> + <? endif ?> +</p> + diff --git a/modules/gallery/views/upgrader.html.php b/modules/gallery/views/upgrader.html.php new file mode 100644 index 0000000..4c611f7 --- /dev/null +++ b/modules/gallery/views/upgrader.html.php @@ -0,0 +1,163 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title><?= t("Gallery 3 upgrader") ?></title> + <link rel="stylesheet" type="text/css" href="<?= url::file("modules/gallery/css/upgrader.css") ?>" + media="screen,print,projection" /> + <script src="<?= url::file("lib/jquery.js") ?>" type="text/javascript"></script> + </head> + <body<? if (locales::is_rtl()) { echo ' class="rtl"'; } ?>> + <div id="outer"> + <img id="logo" src="<?= url::file("modules/gallery/images/gallery.png") ?>" /> + <div id="inner"> + <? if ($can_upgrade): ?> + <div id="dialog" style="visibility: hidden"> + <a id="dialog_close_link" style="display: none" onclick="$('#dialog').fadeOut(); return false;" href="#" class="close">[x]</a> + <div id="busy" style="display: none"> + <h1> + <img width="16" height="16" src="<?= url::file("themes/wind/images/loading-small.gif") ?>"/> + <?= t("Upgrade in progress!") ?> + </h1> + <p> + <?= t("Please don't refresh or leave the page.") ?> + </p> + </div> + <div id="done" style="display: none"> + <h1> <?= t("That's it!") ?> </h1> + <p> + <?= t("Your Gallery is up to date.<br/><a href=\"%url\">Return to your Gallery</a>", + array("url" => html::mark_clean(url::base()))) ?> + </p> + </div> + <div id="failed" style="display: none"> + <h1> <?= t("Some modules failed to upgrade!") ?> </h1> + <p> + <?= t("Failed modules are <span class=\"failed\">highlighted</span>. Try getting newer versions or <a href=\"%admin_modules\">deactivating those modules</a>.", array("admin_modules" => url::site("admin/modules"))) ?> + </p> + </div> + </div> + <script type="text/javascript"> + $(document).ready(function() { + $("#dialog").css("left", Math.round(($(window).width() - $("#dialog").width()) / 2)); + $("#dialog").css("top", Math.round(($(window).height() - $("#dialog").height()) / 2)); + $("#upgrade_link").click(function(event) { show_busy() }); + + <? if ($done): ?> + show_done(); + <? endif ?> + + <? if ($failed): ?> + show_failed(); + <? endif ?> + }); + + var show_busy = function() { + $("#dialog").css("visibility", "visible"); + $("#busy").show(); + $("#upgrade_link").parent().removeClass("button-active"); + $("#upgrade_link").replaceWith($("#upgrade_link").html()) + } + + var show_done = function() { + $("#dialog").css("visibility", "visible"); + $("#done").show(); + $("#dialog_close_link").show(); + } + + var show_failed = function() { + $("#dialog").css("visibility", "visible"); + $("#failed").show(); + $("#dialog_close_link").show(); + } + </script> + <div id="welcome_message"> + <p class="<?= $done ? "muted" : "" ?>"> + <?= t("Welcome to the Gallery upgrader. One click and you're done!") ?> + </p> + </div> + + <? if ($done): ?> + <div id="upgrade_button" class="button muted"> + <?= t("Upgrade all") ?> + </div> + <? else: ?> + <div id="upgrade_button" class="button button-active"> + <a id="upgrade_link" href="<?= url::site("upgrader/upgrade?csrf=" . access::csrf_token()) ?>"> + <?= t("Upgrade all") ?> + </a> + </div> + <? endif ?> + + <? if ($obsolete_modules_message): ?> + <div id="obsolete_modules_message"> + <p> + <span class="failed"><?= t("Warning!") ?></span> + <?= $obsolete_modules_message ?> + </p> + </div> + <? endif ?> + + <table> + <tr class="<?= $done ? "muted" : "" ?>"> + <th class="name"> <?= t("Module name") ?> </th> + <th> <?= t("Installed version") ?> </th> + <th> <?= t("Available version") ?> </th> + </tr> + + <? foreach ($available as $id => $module): ?> + <? if ($module->active): ?> + <tr class="<?= $module->version == $module->code_version ? "current" : "upgradeable" ?> <?= in_array($id, $failed) ? "failed" : "" ?>" > + <td class="name <?= $id ?>"> + <?= t($module->name) ?> + </td> + <td> + <?= $module->version ?> + </td> + <td> + <?= $module->code_version ?> + </td> + </tr> + <? else: ?> + <? @$inactive++ ?> + <? endif ?> + <? endforeach ?> + </table> + + <? if (@$inactive): ?> + <p class="<?= $done ? "muted" : "" ?>"> + <?= t("The following modules are inactive and don't require an upgrade.") ?> + </p> + <ul class="<?= $done ? "muted" : "" ?>"> + <? foreach ($available as $module): ?> + <? if (!$module->active): ?> + <li> + <?= t($module->name) ?> + </li> + <? endif ?> + <? endforeach ?> + </ul> + <? endif ?> + <? 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 <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 ?> + </div> + <div id="footer"> + <p> + <em> + <?= t("Did something go wrong? Try the <a href=\"%faq_url\">FAQ</a> or ask in the <a href=\"%forums_url\">Gallery forums</a>.", + array("faq_url" => "http://codex.galleryproject.org/Gallery3:FAQ", + "forums_url" => "http://galleryproject.org/forum")) ?> + </em> + </p> + </div> + </div> + </body> +</html> diff --git a/modules/gallery/views/user_languages_block.html.php b/modules/gallery/views/user_languages_block.html.php new file mode 100644 index 0000000..3776ca1 --- /dev/null +++ b/modules/gallery/views/user_languages_block.html.php @@ -0,0 +1,19 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= form::dropdown("g-select-session-locale", $installed_locales, $selected) ?> +<script type="text/javascript"> + $("select[name=g-select-session-locale]").change(function() { + var old_locale_preference = <?= html::js_string($selected) ?>; + var locale = $(this).val(); + if (old_locale_preference == locale) { + return; + } + + var expires = -1; + if (locale) { + expires = 365; + } + $.cookie("g_locale", locale, {"expires": expires, "path": "/"}); + window.location.reload(true); + }); +</script> + diff --git a/modules/gallery/views/user_profile.html.php b/modules/gallery/views/user_profile.html.php new file mode 100644 index 0000000..257bd7c --- /dev/null +++ b/modules/gallery/views/user_profile.html.php @@ -0,0 +1,47 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $(document).ready(function() { + $("#g-profile-return").click(function(event) { + history.go(-1); + return false; + }) + }); +</script> +<div id="g-user-profile"> + <div class="ui-helper-clearfix"> + <a id="g-profile-return" class="g-button g-right ui-state-default ui-corner-all" href="#"> + <?= t("Return") ?> + </a> + <? if ($editable): ?> + <a class="g-button g-right ui-state-default ui-corner-all g-dialog-link" href="<?= url::site("users/form_change_email/{$user->id}") ?>"> + <?= t("Change email") ?> + </a> + <a class="g-button g-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 g-right ui-state-default ui-corner-all g-dialog-link" href="<?= url::site("form/edit/users/{$user->id}") ?>"> + <?= t("Edit") ?> + </a> + <? endif ?> + <? if ($contactable): ?> + <a class="g-button g-right ui-state-default ui-corner-all g-dialog-link" + href="<?= url::site("user_profile/contact/{$user->id}") ?>"> + <?= t("Contact") ?> + </a> + <? endif ?> + </div> + <h1> + <img src="<?= $user->avatar_url(40, $theme->url("images/avatar.jpg", true)) ?>" + alt="<?= html::clean_attribute($user->display_name()) ?>" + class="g-avatar g-left" width="40" height="40" /> + <?= t("User profile: %name", array("name" => $user->display_name())) ?> + </h1> + <? foreach ($info_parts as $info): ?> + <div class="g-block"> + <h2><?= html::purify($info->title) ?></h2> + <div class="g-block-content"> + <?= $info->view ?> + </div> + </div> + <? endforeach ?> +</div> diff --git a/modules/gallery/views/user_profile_info.html.php b/modules/gallery/views/user_profile_info.html.php new file mode 100644 index 0000000..e559abd --- /dev/null +++ b/modules/gallery/views/user_profile_info.html.php @@ -0,0 +1,9 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<table> + <? foreach ($user_profile_data as $label => $value): ?> + <tr> + <th><?= html::clean($label) ?></th> + <td><?= html::purify($value) ?></td> + </tr> + <? endforeach ?> +</table> diff --git a/modules/gallery/views/welcome_message.html.php b/modules/gallery/views/welcome_message.html.php new file mode 100644 index 0000000..bb6b4a8 --- /dev/null +++ b/modules/gallery/views/welcome_message.html.php @@ -0,0 +1,36 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-welcome-message"> + <h1 style="display: none"> + <?= t("Welcome to Gallery 3!") ?> + </h1> + + <p> + <h2> + <?= t("Congratulations on choosing Gallery to host your photos. You're going to have a great experience!") ?> + </h2> + </p> + + <p> + <?= t("First things first. You're logged in to the <b>%user_name</b> account. You should change your password to something that you'll remember.", array("user_name" => $user->name)) ?> + </p> + + <p> + <a href="<?= url::site("admin/users/edit_user_form/{$user->id}") ?>" + title="<?= t("Edit your profile")->for_html_attr() ?>" + id="g-after-install-change-password-link" + class="g-button ui-state-default ui-corner-all"> + <?= t("Change password and email now") ?> + </a> + <script type="text/javascript"> + $("#g-after-install-change-password-link").gallery_dialog(); + </script> + </p> + + <p> + <?= t("Want to learn more? The <a href=\"%url\">Gallery website</a> has news and information about the Gallery project and community.", array("url" => "http://galleryproject.org")) ?> + </p> + + <p> + <?= t("Having problems? There's lots of information in our <a href=\"%codex_url\">documentation site</a> or you can <a href=\"%forum_url\">ask for help in the forums!</a>", array("codex_url" => "http://codex.galleryproject.org/Main_Page", "forum_url" => "http://galleryproject.org/forum")) ?> + </p> +</div> diff --git a/modules/gallery/views/welcome_message_loader.html.php b/modules/gallery/views/welcome_message_loader.html.php new file mode 100644 index 0000000..d1ff2f3 --- /dev/null +++ b/modules/gallery/views/welcome_message_loader.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<span id="g-welcome-message-link" + title="<?= t("Welcome to Gallery 3")->for_html_attr() ?>" + href="<?= url::site("welcome_message") ?>"/> +<script type="text/javascript"> + $(document).ready(function(){$("#g-welcome-message-link").gallery_dialog({immediate: true});}); +</script> diff --git a/modules/greydragon/changelog.txt b/modules/greydragon/changelog.txt new file mode 100644 index 0000000..ae8baf9 --- /dev/null +++ b/modules/greydragon/changelog.txt @@ -0,0 +1,11 @@ +GreyDragon Shared Module Changelog
+
+version 1.3:
+- Added check to detect situation when CURL is not installed to disable Auto Update feature
+- Improved CURL logic to properly handle redirects
+
+version 1.2:
+- Fixed issue with some installations not taking relative path for CSS
+
+version 1.1:
+- Initial release
\ No newline at end of file diff --git a/modules/greydragon/css/gd_common.css b/modules/greydragon/css/gd_common.css new file mode 100644 index 0000000..32b780b --- /dev/null +++ b/modules/greydragon/css/gd_common.css @@ -0,0 +1,59 @@ +/**
+ * Gallery 3 Grey Dragon Common Module
+ * Copyright (C) 2012 Serguei Dosyukov
+ *
+ * CSS rules for admin section
+ */
+
+body { min-width: 1200px; }
+
+#g-content { font-size: 1em; margin-bottom: 0; width: auto; padding-left: 1em; padding-right: 1em; }
+#g-content ul { margin-bottom: 0; }
+
+#g-content h3 { color: #d54e21; border-bottom: #a2bdbf 1px solid; margin-top: 0.3em; margin-bottom: 0.3em; }
+#g-content p { color: #333; }
+#g-content table { margin-bottom: 0; }
+
+#g-content input { display: inline; float: left; margin-right: 0.8em; color: #555555; border: 1px solid #ccc; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; }
+#g-content textarea { height: 6em; color: #555555; border: 1px solid #ccc; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; }
+#g-content select { display: inline; float: left; margin-right: 0.8em; width: 50.6%; color: #555555; border: 1px solid #ccc; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; }
+
+#g-content input[type='checkbox'] { border: none; }
+#g-content input[type='text'] { width: 50%; }
+
+#g-content input.submit { display: inline-block; min-width: 100px; padding: 4px 10px 4px; font-size: 13px; line-height: 18px; color:#333333; text-align: center; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); background-color: #fafafa; background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); background-repeat: no-repeat; filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); border: 1px solid #ccc; border-bottom-color: #bbb; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
+#g-content input.submit:first-child { *margin-left: 0; }
+#g-content input.submit:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
+
+#g-content input.g-error { padding-left: 30px; border: none; }
+#g-content input.g-success { background-color: transparent; }
+#g-content input.g-warning { background-color: transparent; border: none; }
+
+#g-content p.g-error { padding-left: 30px; border: none; margin-bottom: 0; background-image: none; }
+
+#gd-admin-header { padding: 7px 0; margin: 4px 0 0 0; background-color: #fbfbfb; background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); background-image:- ms-linear-gradient(top, #ffffff, #f5f5f5); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); background-image: linear-gradient(top, #ffffff, #f5f5f5); background-repeat: repeat-x; filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); border: 1px solid #ddd; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; display: inline-block; width: 100%; }
+#gd-admin-header .divider{padding:0 5px;color:#999999;}
+#gd-admin-header .active a{color:#333333;}
+
+#gd-admin-version,
+#gd-admin-version-2 { margin-top: 4px; padding: 7px 14px; background-color: rgb(217, 237, 247); border: 1px solid #bce8f1; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; }
+
+#gd-admin-title { float: left; padding-left: 10px; color: #333v42; font-weight: bold; font-size: 1.4em; text-shadow: #deeefa 0 1px 0; display: inline-block; }
+#gd-admin-hlinks { float: right; padding-right: 10px; }
+#gd-admin-hlinks li { list-style-type: none; float: left; color: #618299; display: inline; text-shadow: 0 1px 0 #ffffff; }
+#gd-admin-hlinks a { line-height: 1.6em; }
+#gd-admin-hlinks a[disabled="disabled"], #gd-admin-hlinks a[disabled="disabled"]:hover { text-decoration: none; cursor: default; }
+
+#g-autoupdate-config { display: none; border: 1px solid #ddd; border-top: none; width: 45%; height: 2.5em; margin-left: 54%; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; }
+#g-autoupdate-config.visible { min-height: 30px; display: inline-block; }
+#g-autoupdate-config ul { float: right; margin: 6px 10px;}
+#g-autoupdate-config li { float: left; display: inline; line-height: 1.5em; }
+
+#g-admin-container { margin-top: 14px; font-size: 0.9em; line-height: 1.6em; }
+#g-admin-container .column1 { float: left; width: 53%; min-width: 610px; }
+#g-admin-container .column2 { float: right; width: 46%; min-width: 529px; }
+
+#g-admin-container fieldset { position: relative; border-top-left-radius: 0.4em; border-top-right-radius: 0.4em; overflow: hidden; }
+#g-admin-container legend { position: absolute; left: 0; width: 100%; padding: 0.4em 0.8em; background: url(../images/blue-grad.png) #d5e6f2 repeat-x left top; border-bottom: #dfdfdf 1px solid; }
+#g-admin-container fieldset ul { margin-top: 34px; }
+
diff --git a/modules/greydragon/images/blue-grad.png b/modules/greydragon/images/blue-grad.png Binary files differnew file mode 100644 index 0000000..868a657 --- /dev/null +++ b/modules/greydragon/images/blue-grad.png diff --git a/modules/greydragon/module.info b/modules/greydragon/module.info new file mode 100644 index 0000000..4a65e8c --- /dev/null +++ b/modules/greydragon/module.info @@ -0,0 +1,9 @@ +name = "GreyDragon Shared"
+description = "Shared content for modules from GreyDragon. Need to be activated.<br />Version 1.3 | By <a href=http://blog.dragonsoft.us>Serguei Dosyukov</a>"
+version = 13
+author_name = "Serguei Dosyukov"
+author_url = "http://blog.dragonsoft.us/gallery-3/"
+info_url = ""
+discuss_url = ""
+
+
diff --git a/modules/greydragon/views/gd_admin_include.html.php b/modules/greydragon/views/gd_admin_include.html.php new file mode 100644 index 0000000..73962cb --- /dev/null +++ b/modules/greydragon/views/gd_admin_include.html.php @@ -0,0 +1,180 @@ +<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Grey Dragon Theme - a custom theme for Gallery 3
+ * This theme was designed and built by Serguei Dosyukov, whose blog you will find at http://blog.dragonsoft.us
+ * Copyright (C) 2012 Serguei Dosyukov
+ *
+ * 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.
+ */
+?>
+<style>
+ @import "<?= url::file("modules/greydragon/css/gd_common.css"); ?>";
+</style>
+
+<script>
+ $(document).ready( function() {
+ $('form').submit( function() { $('input[type=submit]', this).attr('disabled', 'disabled'); });
+
+ var objAutoUpdate = $('#g-autoupdate-config');
+ var objAutoHidden = $('input[name="g_auto_delay"]');
+ var objAutoEdit = $('#g-auto-delay-edit');
+ function showSidebar(){ objAutoEdit.val(objAutoHidden.val()); objAutoUpdate.slideDown('fast', function() { objAutoUpdate.addClass('visible'); }); };
+ function hideSidebar(){ objAutoHidden.val(objAutoEdit.val()); objAutoUpdate.slideUp('fast', function() { objAutoUpdate.removeClass('visible'); }); };
+
+ objAutoEdit.keyup( function() { objAutoHidden.val($(this).val()); });
+ objAutoEdit.keydown(function(event) {
+ // Allow: backspace, delete, tab and escape
+ if ( event.keyCode == 46 || event.keyCode == 8 || event.keyCode == 9 || event.keyCode == 27 ||
+ // Allow: Ctrl+A
+ (event.keyCode == 65 && event.ctrlKey === true) ||
+ // Allow: home, end, left, right
+ (event.keyCode >= 35 && event.keyCode <= 39)) {
+ // let it happen, don't do anything
+ return;
+ } else {
+ // Ensure that it is a number and stop the keypress
+ if ((event.keyCode < 48 || event.keyCode > 57) && (event.keyCode < 96 || event.keyCode > 105 )) {
+ event.preventDefault();
+ }
+ }
+ });
+
+ $('.g-link-autoupdate').click(function(e){ e.preventDefault(); if ( objAutoUpdate.hasClass('visible') ){ hideSidebar(); } else { showSidebar(); }});
+ });
+</script>
+
+<?
+
+function isCurlInstalled() {
+ if (in_array('curl', get_loaded_extensions())) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+// -1 - cannot get version info
+// 0 - current
+// + - newer is avaialble, version is returned
+function checkVersionInfo($downloadid, $version) {
+ if (!isset($downloadid)):
+ return -1;
+ endif;
+
+ try {
+ $call = "http://blog.dragonsoft.us/downloadversion/" . $downloadid;
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $call);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
+ curl_setopt($ch, CURLOPT_MAXREDIRS, 1);
+ $output=curl_exec($ch);
+ $json = json_decode($output);
+
+ if ($json->id == $downloadid):
+ $newversion = $json->version;
+ if ($json->version > $version):
+ return $json->version;
+ else:
+ return 0;
+ endif;
+ else:
+ return -1;
+ endif;
+ } catch (Exception $e) {
+ return -1;
+ }
+}
+
+if ($is_module):
+ $admin_info = new ArrayObject(parse_ini_file(MODPATH . $name . "/module.info"), ArrayObject::ARRAY_AS_PROPS);
+ $version = number_format($admin_info->version / 10, 1, '.', '');
+ $lastupdate = module::get_var($name, "last_update", time());
+ $checkInDays = module::get_var($name, "auto_delay", 30);
+else:
+ $admin_info = new ArrayObject(parse_ini_file(THEMEPATH . $name . "/theme.info"), ArrayObject::ARRAY_AS_PROPS);
+ $version = $admin_info->version;
+ $lastupdate = module::get_var("th_" . $name, "last_update", time());
+ $checkInDays = module::get_var("th_" . $name, "auto_delay", 30);
+endif;
+
+if (isCurlInstalled() && ($checkInDays > 0) && ((time() - $lastupdate) > ($checkInDays * 24 * 60 * 60))): // Check version every N days
+ $admin_info2 = new ArrayObject(parse_ini_file(MODPATH . "greydragon/module.info"), ArrayObject::ARRAY_AS_PROPS);
+ $version2 = number_format($admin_info2->version / 10, 1, '.', '');
+
+ $versionCheck = checkVersionInfo($downloadid, $version);
+ $versionCheck2 = checkVersionInfo(15, $version2);
+
+ if (($versionCheck == 0) && ($versionCheck2 == 0)):
+ if ($is_module):
+ module::set_var($name, "last_update", time());
+ else:
+ module::set_var("th_" . $name, "last_update", time());
+ endif;
+ endif;
+else:
+ $versionCheck = 0;
+ $versionCheck2 = 0;
+endif;
+?>
+
+<div id="gd-admin-header">
+ <div id="gd-admin-title"><?= t($admin_info->name) ?> - <?= $version ?></div>
+ <div id="gd-admin-hlinks">
+ <ul style="float: right;"><li><a href="http://blog.dragonsoft.us/gallery-3/" target="_blank"><?= t("Home") ?></a> | </li>
+ <? if (isset($admin_info->discuss_url)): ?>
+ <li><a href="<?= $admin_info->discuss_url; ?>" target="_blank"><?= t("Support") ?></a> | </li>
+ <? endif; ?>
+ <? if (isset($admin_info->info_url)): ?>
+ <li><a href="<?= $admin_info->info_url; ?>" target="_blank"><?= t("Download") ?></a> | </li>
+ <? endif; ?>
+ <? if (isset($admin_info->vote)): ?>
+ <li><a href="<?= $admin_info->vote; ?>" target="_blank"><?= t("Vote") ?></a> | </li>
+ <? endif; ?>
+ <li><a href="http://twitter.com/greydragon_th" target="_blank" title="<?= t("Follow Us on Twitter") ?>"><?= t("Follow Us") ?></a> | </li>
+ <li><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9MWBSVJMWMJEU" target="_blank" ><?= t("Coffee Fund") ?></a> | </li>
+ <li><a href="#" class="g-link-autoupdate" <?= (isCurlInstalled())? null : "disabled=\"disabled\""; ?> ><?= t("Auto Update"); ?></a></li>
+ </ul>
+ </div>
+</div>
+<div id="g-autoupdate-config">
+ <ul><li><?= t("Check every"); ?> </li>
+ <li><input id="g-auto-delay-edit" type="text" size="2" value="30"></li>
+ <li><?=t("days (set to 0 to disable)"); ?></li>
+<? if (($versionCheck == 0) && ($versionCheck2 == 0)): ?>
+ <li> | <?= t("Last check:"); ?> <?= date("Y-m-d H:i:s", $lastupdate); ?></li>
+<? endif; ?>
+ </ul>
+</div>
+
+<? if ($versionCheck == -1): ?>
+ <div id="gd-admin-version"><?= t("Version check is incomplete. No version information has been found."); ?> <?= $versionCheck; ?> : <?= $downloadid; ?></div>
+<? elseif ($versionCheck == 0): ?>
+<? else: ?>
+ <div id="gd-admin-version"><?= t("Newer version") ?> <?= $versionCheck; ?> <?= t("is available. Click Download link for more info.") ?></div>
+<? endif; ?>
+<? if (($versionCheck2 == -1) || ($versionCheck2 == 0)): ?>
+<? else: ?>
+ <div id="gd-admin-version-2"><?= t("Newer version") ?> <?= $versionCheck2; ?> <?= t("of GreyDragon Shared Module is available. Click") . ' <a href="http://codex.gallery2.org/Gallery3:Modules:greydragon" target="_blank">' . t("here") . '</a> ' . t("for more info.") ?></div>
+<? endif; ?>
+<div id="g-admin-container">
+<? if (isset($help)): ?>
+ <div class="column1">
+ <?= $form ?>
+ </div>
+ <div class="column2">
+ <?= $help ?>
+ </div>
+<? else: ?>
+ <?= $form ?>
+<? endif; ?>
+</div>
diff --git a/modules/html5_uploader/controllers/uploader.php b/modules/html5_uploader/controllers/uploader.php new file mode 100644 index 0000000..8f839a0 --- /dev/null +++ b/modules/html5_uploader/controllers/uploader.php @@ -0,0 +1,126 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Uploader_Controller extends Controller { + public function index($id) { + $album = ORM::factory("item", $id); + access::required("view", $album); + access::required("add", $album); + if (!$album->is_album()) { + $album = $album->parent(); + } + + print $this->_get_add_form($album); + } + + public function add($id) { + $album = ORM::factory("item", $id); + access::required("view", $album); + access::required("add", $album); + access::verify_csrf(); + + $form = $this->_get_add_form($album); + if ($form->validate()) { + batch::start(); + + $count = 0; + $added_a_movie = false; + $added_a_photo = false; + + $files_list=$_FILES['files']; + foreach (array_keys($files_list['name']) as $index) { + try { + $temp_filename = $files_list['tmp_name'][$index]; + $item = ORM::factory("item"); + $item->name = basename($files_list['name'][$index]); + $item->title = item::convert_filename_to_title($item->name); + $item->parent_id = $album->id; + $item->set_data_file($temp_filename); + + $path_info = @pathinfo($item->name); + if (array_key_exists("extension", $path_info) && + in_array(strtolower($path_info["extension"]), array("flv", "mp4", "m4v"))) { + $item->type = "movie"; + $item->save(); + $added_a_movie = true; + log::success("content", t("Added a movie"), + html::anchor("movies/$item->id", t("view movie"))); + } else { + $item->type = "photo"; + $item->save(); + $added_a_photo = true; + log::success("content", t("Added a photo"), + html::anchor("photos/$item->id", t("view photo"))); + } + $count++; + module::event("add_photos_form_completed", $item, $form); + } catch (Exception $e) { + // Lame error handling for now. Just record the exception and move on + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + + // Ugh. I hate to use instanceof, But this beats catching the exception separately since + // we mostly want to treat it the same way as all other exceptions + if ($e instanceof ORM_Validation_Exception) { + Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1)); + } + } + + if (file_exists($temp_filename)) { + unlink($temp_filename); + } + } + batch::stop(); + if ($count) { + if ($added_a_photo && $added_a_movie) { + message::success(t("Added %count photos and movies", array("count" => $count))); + } else if ($added_a_photo) { + message::success(t2("Added one photo", "Added %count photos", $count)); + } else { + message::success(t2("Added one movie", "Added %count movies", $count)); + } + } + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + + // Override the application/json mime type. The dialog based HTML uploader uses an iframe to + // buffer the reply, and on some browsers (Firefox 3.6) it does not know what to do with the + // JSON that it gets back so it puts up a dialog asking the user what to do with it. So force + // the encoding type back to HTML for the iframe. + // See: http://jquery.malsup.com/form/#file-upload + header("Content-Type: text/html; charset=" . Kohana::CHARSET); + } + + private function _get_add_form($album) { + $form = new Forge("uploader/add/{$album->id}", "", "post", array("id" => "g-add-photos-form")); + $group = $form->group("add_photos") + ->label(t("Add photos to %album_title", array("album_title" => html::purify($album->title)))); + $group->input("files[]")->type("file")->multiple(); + + $form->input("FOO")->type("hidden")->label(sprintf("Es können mehrere Bilder auf einmal hochgeladen werden. Dies dauert allerdings etwas - ohne extra Ladebalken. Also Geduld bewahren. Max. upload size of all pictures: %.0f MB.", ini_get("upload_max_filesize"))); + + module::event("add_photos_form", $album, $form); + + $group = $form->group("buttons")->label(""); + $group->submit("")->value(t("Upload")); + + return $form; + } +} diff --git a/modules/html5_uploader/module.info b/modules/html5_uploader/module.info new file mode 100644 index 0000000..542de3e --- /dev/null +++ b/modules/html5_uploader/module.info @@ -0,0 +1,7 @@ +name = "HTML5 Uploader" +description = "Simple HTML uploader that replaces the Flash based uploader" +version = 1 +author_name = "" +author_url = "" +info_url = "http://codex.gallery2.org/Gallery3:Modules:html_uploader" +discuss_url = "http://gallery.menalto.com/forum_module_html_uploader" diff --git a/modules/image_block/controllers/image_block.php b/modules/image_block/controllers/image_block.php new file mode 100644 index 0000000..3198970 --- /dev/null +++ b/modules/image_block/controllers/image_block.php @@ -0,0 +1,27 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Image_Block_Controller extends Controller { + public function random($item_id) { + $item = ORM::factory("item", $item_id); + access::required("view", $item); + item::set_display_context_callback("Albums_Controller::get_display_context"); + url::redirect($item->abs_url()); + } +} diff --git a/modules/image_block/helpers/image_block_block.php b/modules/image_block/helpers/image_block_block.php new file mode 100644 index 0000000..37e5b4c --- /dev/null +++ b/modules/image_block/helpers/image_block_block.php @@ -0,0 +1,56 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class image_block_block_Core { + static function get_site_list() { + return array("random_image" => t("Random image")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "random_image": + // 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. + $image_count = module::get_var("image_block", "image_count"); + $items = array(); + for ($i = 0; $i < $image_count; $i++) { + $attempts = 0; + $item = null; + do { + $item = item::random_query()->where("type", "!=", "album")->find_all(1)->current(); + } while (!$item && $attempts++ < 3); + if ($item) { + $items[] = $item; + } + } + if ($items) { + $block = new Block(); + $block->css_id = "g-image-block"; + $block->title = t2("Random image", "Random images", $image_count); + $block->content = new View("image_block_block.html"); + $block->content->items = $items; + } + break; + } + + return $block; + } +} diff --git a/modules/image_block/helpers/image_block_installer.php b/modules/image_block/helpers/image_block_installer.php new file mode 100644 index 0000000..b177b97 --- /dev/null +++ b/modules/image_block/helpers/image_block_installer.php @@ -0,0 +1,43 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class image_block_installer { + + static function install() { + module::set_var("image_block", "image_count", "1"); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + module::set_var("image_block", "image_count", "1"); + module::set_version("image_block", $version = 2); + } + + // Oops, there was a bug in the installer for version 2 resulting + // in some folks not getting the image_count variable set. Bump + // to version 3 and fix it. + if ($version == 2) { + if (module::get_var("image_block", "image_count", 0) === 0) { + module::set_var("image_block", "image_count", "1"); + } + module::set_version("image_block", $version = 3); + } + } +} diff --git a/modules/image_block/module.info b/modules/image_block/module.info new file mode 100644 index 0000000..25b89e6 --- /dev/null +++ b/modules/image_block/module.info @@ -0,0 +1,7 @@ +name = "Image Block" +description = "Display a random image in the sidebar" +version = 3 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:image_block" +discuss_url = "http://galleryproject.org/forum_module_image_block" diff --git a/modules/image_block/views/image_block_block.html.php b/modules/image_block/views/image_block_block.html.php new file mode 100644 index 0000000..6f68e5b --- /dev/null +++ b/modules/image_block/views/image_block_block.html.php @@ -0,0 +1,8 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? foreach ($items as $item): ?> +<div class="g-image-block"> + <a href="<?= url::site("image_block/random/" . $item->id); ?>"> + <?= $item->thumb_img(array("class" => "g-thumbnail")) ?> + </a> +</div> +<? endforeach ?> diff --git a/modules/info/helpers/info_block.php b/modules/info/helpers/info_block.php new file mode 100644 index 0000000..e4b5adf --- /dev/null +++ b/modules/info/helpers/info_block.php @@ -0,0 +1,93 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class info_block_Core { + static function get_site_list() { + return array("metadata" => t("Metadata")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "metadata": + if ($theme->item()) { + $block = new Block(); + $block->css_id = "g-metadata"; + $block->title = $theme->item()->is_album() ? t("Album info") : + ($theme->item()->is_movie() ? t("Movie info") : t("Photo info")); + $block->content = new View("info_block.html"); + if ($theme->item->title && module::get_var("info", "show_title")) { + $info["title"] = array( + "label" => t("Title:"), + "value" => html::purify($theme->item->title) + ); + } + if ($theme->item->description && module::get_var("info", "show_description")) { + $info["description"] = array( + "label" => t("Description:"), + "value" => nl2br(html::purify($theme->item->description)) + ); + } + if (!$theme->item->is_album() && module::get_var("info", "show_name")) { + $info["file_name"] = array( + "label" => t("File name:"), + "value" => html::clean($theme->item->name) + ); + } + if ($theme->item->captured && module::get_var("info", "show_captured")) { + $info["captured"] = array( + "label" => t("Captured:"), + "value" => gallery::date_time($theme->item->captured) + ); + } + if ($theme->item->owner && module::get_var("info", "show_owner")) { + $display_name = $theme->item->owner->display_name(); + if ($theme->item->owner->url) { + $info["owner"] = array( + "label" => t("Owner:"), + "value" => html::anchor( + html::clean($theme->item->owner->url), + html::clean($display_name)) + ); + } else { + $info["owner"] = array( + "label" => t("Owner:"), + "value" => html::clean($display_name) + ); + } + } + if (($theme->item->width && $theme->item->height) && + module::get_var("info", "show_dimensions")) { + $info["size"] = array( + "label" => t("Dimensions:"), + "value" => t( + "%width x %height px", + array("width" => $theme->item->width, "height" => $theme->item->height)) + ); + } + + $block->content->metadata = $info; + + module::event("info_block_get_metadata", $block, $theme->item); + } + break; + } + return $block; + } +}
\ No newline at end of file diff --git a/modules/info/helpers/info_installer.php b/modules/info/helpers/info_installer.php new file mode 100644 index 0000000..2d06a0e --- /dev/null +++ b/modules/info/helpers/info_installer.php @@ -0,0 +1,45 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class info_installer { + + static function install() { + module::set_var("info", "show_title", 1); + module::set_var("info", "show_description", 1); + module::set_var("info", "show_owner", 1); + module::set_var("info", "show_name", 1); + module::set_var("info", "show_captured", 1); + module::set_var("info", "show_dimensions", 1); + } + + static function upgrade($version) { + if ($version == 1) { + module::set_var("info", "show_title", 1); + module::set_var("info", "show_description", 1); + module::set_var("info", "show_owner", 1); + module::set_var("info", "show_name", 1); + module::set_var("info", "show_captured", 1); + module::set_version("info", $version = 2); + } + if ($version == 2) { + module::set_var("info", "show_dimensions", 1); + module::set_version("info", $version = 3); + } + } +} diff --git a/modules/info/helpers/info_theme.php b/modules/info/helpers/info_theme.php new file mode 100644 index 0000000..457d330 --- /dev/null +++ b/modules/info/helpers/info_theme.php @@ -0,0 +1,41 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class info_theme_Core { + static function thumb_info($theme, $item) { + $results = ""; + if ($item->view_count) { + $results .= "<li>"; + $results .= t("Views: %view_count", array("view_count" => $item->view_count)); + $results .= "</li>"; + } + if ($item->owner) { + $results .= "<li>"; + if ($item->owner->url) { + $results .= t("By: <a href=\"%owner_url\">%owner_name</a>", + array("owner_name" => $item->owner->display_name(), + "owner_url" => $item->owner->url)); + } else { + $results .= t("By: %owner_name", array("owner_name" => $item->owner->display_name())); + } + $results .= "</li>"; + } + return $results; + } +}
\ No newline at end of file diff --git a/modules/info/module.info b/modules/info/module.info new file mode 100644 index 0000000..33b1622 --- /dev/null +++ b/modules/info/module.info @@ -0,0 +1,7 @@ +name = "Info" +description = "Display extra information about photos and albums" +version = 3 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:info" +discuss_url = "http://galleryproject.org/forum_module_info" diff --git a/modules/info/views/info_block.html.php b/modules/info/views/info_block.html.php new file mode 100644 index 0000000..b296fa1 --- /dev/null +++ b/modules/info/views/info_block.html.php @@ -0,0 +1,8 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul class="g-metadata"> + <? foreach($metadata as $info): ?> + <li> + <strong class="caption"><?= $info["label"] ?></strong> <?= $info["value"] ?> + </li> + <? endforeach; ?> +</ul> diff --git a/modules/kbd_nav/changelog.txt b/modules/kbd_nav/changelog.txt new file mode 100644 index 0000000..148e1e5 --- /dev/null +++ b/modules/kbd_nav/changelog.txt @@ -0,0 +1,47 @@ +Kbd Navigation Changelog
+
+version 2.1:
+- Fixed issue with jQuery extension not always initialized
+- New Shortcodes:
+ Shift + C - Go to Calendar Page - Album/Photo page
+ Shift + E - Open Edit Dialog - Album/Photo page
+ Shift + F - Open Full Size - Photo page
+ Shift + I - Open Exif Info dialog - Photo page
+ Shift + S - Go to Search Box
+ Shift + V - Thumb/Photo Click to open Slideshow, where supported
+
+version 2.0:
+- Module info adjusted to match new format in G3 3.0.2+
+- Added support for ColorBox
+
+version 1.9:
+- Code adjusments to comply with new G3 requirements
+
+version 1.8:
+- Added logic to prevent navigation when Fancybox is opened.
+
+version 1.7:
+- Added logic further protecting user input activities (ex: comments)
+
+version 1.6:
+- Added logic to prevent navigation when dialogs are opened.
+- Changed detection of Shadowbox preview overlay
+
+version 1.5:
+- Fix for RTL detection
+- Added support for Wind theme
+
+version 1.4:
+- Added RTL detection
+
+version 1.3:
+- Internal revision
+
+version 1.2:
+- Added support for GreyDragon Photo Slideshow navigation - in Photo SB slideshow mode, key navigation is superseded by slideshow navigation.
+
+version 1.1:
+- Internal revision
+
+version 1.0:
+- Initial release
\ No newline at end of file diff --git a/modules/kbd_nav/helpers/kbd_nav_theme.php b/modules/kbd_nav/helpers/kbd_nav_theme.php new file mode 100644 index 0000000..c539570 --- /dev/null +++ b/modules/kbd_nav/helpers/kbd_nav_theme.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access."); + +class Kbd_Nav_theme_Core { + static function head($theme) { + return $theme->script("kbd_nav.js"); + } +}
\ No newline at end of file diff --git a/modules/kbd_nav/js/kbd_nav.js b/modules/kbd_nav/js/kbd_nav.js new file mode 100644 index 0000000..8a22d98 --- /dev/null +++ b/modules/kbd_nav/js/kbd_nav.js @@ -0,0 +1,120 @@ +/**
+*
+* Copyright (c) 2010-2012 Serguei Dosyukov, http://blog.dragonsoft.us
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
+* files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
+* modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+* Software is furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*
+*/
+
+(function($) {
+ $.fn.KbdNavigation = function(options, callback) {
+ this.options = options || {};
+ var opt = this.options;
+ this.callback = callback || null;
+ var clbk = this.callback;
+
+ $(this).bind("keydown", function(event) {
+ if (event.target.type) { return true; }
+ if (($('div#sb-overlay').is(':visible')) || ($('div#fancybox-overlay').is(':visible')) || ($('div.ui-widget-overlay').is(':visible')) || ($('div#cboxOverlay').is(':visible'))) { return true; }
+
+ var direction = "ltr";
+ if (document.body) {
+ if (window.getComputedStyle) {
+ direction = window.getComputedStyle(document.body, null).direction;
+ } else if (document.body.currentStyle) {
+ direction = document.body.currentStyle.direction;
+ }
+ }
+
+ var lnk = "";
+ var lnk_first, lnk_prev, lnk_parent, lnk_next, lnk_last;
+
+ if(opt.first) { lnk_first = opt.first; } else { lnk_first = $("#g-navi-first").attr("href"); }
+ if(opt.prev) { lnk_prev = opt.prev; } else { lnk_prev = $("#g-navi-prev").attr("href"); }
+ if(opt.parent) { lnk_parent = opt.parent; } else { lnk_parent = $("#g-navi-parent").attr("href"); }
+ if(opt.next) { lnk_next = opt.next; } else { lnk_next = $("#g-navi-next").attr("href"); }
+ if(opt.last) { lnk_last = opt.last; } else { lnk_last = $("#g-navi-last").attr("href"); }
+
+ // Support for standard Wind Theme tags
+ if(!lnk_first) { lnk_first = $(".g-paginator .ui-icon-seek-first").parent().attr("href"); }
+ if(!lnk_prev) { lnk_prev = $(".g-paginator .ui-icon-seek-prev").parent().attr("href"); }
+ if(!lnk_next) { lnk_next = $(".g-paginator .ui-icon-seek-next").parent().attr("href"); }
+ if(!lnk_last) { lnk_last = $(".g-paginator .ui-icon-seek-end").parent().attr("href"); }
+
+ var keyCode = event.keyCode;
+
+ if (direction == "rtl") {
+ switch(keyCode) {
+ case 0x25: // Left
+ keyCode = 0x27;
+ break;
+ case 0x27: // Right
+ keyCode = 0x25;
+ break;
+ }
+ }
+
+ switch(keyCode) {
+ case 0x25: // Ctr+Left/Left
+ if(event.ctrlKey) { lnk = lnk_first; } else { lnk = lnk_prev; }
+ break;
+ case 0x26: // Ctrl+Up
+ if(event.ctrlKey) { lnk = lnk_parent; }
+ break;
+ case 0x27: // Ctrl+Right/Right
+ if(event.ctrlKey) { lnk = lnk_last; } else { lnk = lnk_next; }
+ break;
+ case 0x43: // Shift + C - Go to Calendar Page - Album/Photo page
+ if(event.shiftKey) { lnk = $('#g-view-menu a#g-calendarview-link').attr("href"); }
+ break;
+ case 0x45: // Shift + E - Open Edit Dialog - Album/Photo page
+ if(event.shiftKey) { $('#g-site-menu li a.ui-icon-pencil').click(); return false; }
+ break;
+ case 0x46: // Shift + F - Open Full Size - Photo page
+ if(event.shiftKey) { $('#g-view-menu a.g-fullsize-link').click(); return false; }
+ break;
+ case 0x49: // Shift + I - Open Exif Info dialog - Photo page
+ if(event.shiftKey) { $('#g-view-menu a#g-exifdata-link').click(); return false; }
+ break;
+ case 0x53: // Shift + S - Go to Search Box
+ if(event.shiftKey) { $('#g-quick-search-form input[name="q"]').focus(); return false; }
+ break;
+ case 0x56: // Shift + V - Thumb/Photo Click to open Slideshow, where supported
+ if(event.shiftKey) {
+ $('ul#g-album-grid a.g-sb-preview:visible:first').click();
+ $('#g-photo a.g-sb-preview:not(.g-hide)').click();
+ return false;
+ }
+ break;
+ }
+
+ if(lnk) {
+ if(typeof clbk == 'function') {
+ clbk();
+ return false;
+ } else {
+ window.location = lnk;
+ return true;
+ }
+ }
+ return true;
+ });
+
+ return this;
+ }
+})(jQuery);
+
+$(document).ready( function() {
+ $(document).KbdNavigation({});
+ if ($('#sb-content').is(':visible')) { return true; }
+});
\ No newline at end of file diff --git a/modules/kbd_nav/module.info b/modules/kbd_nav/module.info new file mode 100644 index 0000000..dd18692 --- /dev/null +++ b/modules/kbd_nav/module.info @@ -0,0 +1,8 @@ +name = "Kbd Navigation" +description = "Adds keyboard navigation to the gallery.<br />Version 2.1 | By <a href=http://blog.dragonsoft.us>Serguei Dosyukov</a> | <a href=http://codex.gallery2.org/Gallery3:Modules:kbd_nav>Visit plugin Site</a> | <a href=http://gallery.menalto.com/node/95438>Support</a>" +version = 21 +author_name = "Serguei Dosyukov" +author_url = "http://blog.dragonsoft.us/gallery-3/" +info_url = "http://codex.gallery2.org/Gallery3:Modules:kbd_nav"; +discuss_url = "http://gallery.menalto.com/node/95438"; + diff --git a/modules/kohana23_compat/config/pagination.php b/modules/kohana23_compat/config/pagination.php new file mode 100644 index 0000000..174bc23 --- /dev/null +++ b/modules/kohana23_compat/config/pagination.php @@ -0,0 +1,27 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +$config["default"] = array( + "directory" => "pagination", + "style" => "classic", + "uri_segment" => 3, + "query_string" => "", + "items_per_page" => 20, + "auto_hide" => FALSE +); diff --git a/modules/kohana23_compat/libraries/MY_Database_Builder.php b/modules/kohana23_compat/libraries/MY_Database_Builder.php new file mode 100644 index 0000000..4a09b20 --- /dev/null +++ b/modules/kohana23_compat/libraries/MY_Database_Builder.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Database_Builder extends Database_Builder_Core { + /** + * Merge in a series of where clause tuples and call where() on each one. + * @chainable + */ + public function merge_where($tuples) { + if ($tuples) { + foreach ($tuples as $tuple) { + $this->where($tuple[0], $tuple[1], $tuple[2]); + } + } + return $this; + } + + /** + * Merge in a series of where clause tuples and call or_where() on each one. + * @chainable + */ + public function merge_or_where($tuples) { + if ($tuples) { + foreach ($tuples as $tuple) { + $this->or_where($tuple[0], $tuple[1], $tuple[2]); + } + } + return $this; + } + + public function compile() { + return parent::compile(); + } +}
\ No newline at end of file diff --git a/modules/kohana23_compat/libraries/Pagination.php b/modules/kohana23_compat/libraries/Pagination.php new file mode 100644 index 0000000..bd74a34 --- /dev/null +++ b/modules/kohana23_compat/libraries/Pagination.php @@ -0,0 +1,252 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/** + * Pagination library. + * + * $Id: Pagination.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Pagination_Core { + + // Config values + protected $base_url = ''; + protected $directory = 'pagination'; + protected $style = 'classic'; + protected $uri_segment = 3; + protected $query_string = ''; + protected $items_per_page = 20; + protected $total_items = 0; + protected $auto_hide = FALSE; + + // Autogenerated values + protected $url; + protected $current_page; + protected $total_pages; + protected $current_first_item; + protected $current_last_item; + protected $first_page; + protected $last_page; + protected $previous_page; + protected $next_page; + protected $sql_offset; + protected $sql_limit; + + /** + * Constructs and returns a new Pagination object. + * + * @param array configuration settings + * @return object + */ + public function factory($config = array()) + { + return new Pagination($config); + } + + /** + * Constructs a new Pagination object. + * + * @param array configuration settings + * @return void + */ + public function __construct($config = array()) + { + // No custom group name given + if ( ! isset($config['group'])) + { + $config['group'] = 'default'; + } + + // Pagination setup + $this->initialize($config); + } + + /** + * Sets config values. + * + * @throws Kohana_Exception + * @param array configuration settings + * @return void + */ + public function initialize($config = array()) + { + // Load config group + if (isset($config['group'])) + { + // Load and validate config group + if ( ! is_array($group_config = Kohana::config('pagination.'.$config['group']))) + throw new Kohana_Exception('pagination.undefined_group: ' . $config['group']); + + // All pagination config groups inherit default config group + if ($config['group'] !== 'default') + { + // Load and validate default config group + if ( ! is_array($default_config = Kohana::config('pagination.default'))) + throw new Kohana_Exception('pagination.undefined_group: default'); + + // Merge config group with default config group + $group_config += $default_config; + } + + // Merge custom config items with config group + $config += $group_config; + } + + // Assign config values to the object + foreach ($config as $key => $value) + { + if (property_exists($this, $key)) + { + $this->$key = $value; + } + } + + // Clean view directory + $this->directory = trim($this->directory, '/').'/'; + + // Build generic URL with page in query string + if ($this->query_string !== '') + { + // Extract current page + $this->current_page = isset($_GET[$this->query_string]) ? (int) $_GET[$this->query_string] : 1; + + // Insert {page} placeholder + $_GET[$this->query_string] = '{page}'; + + // Create full URL + $base_url = ($this->base_url === '') ? Router::$current_uri : $this->base_url; + $this->url = url::site($base_url).'?'.str_replace('%7Bpage%7D', '{page}', http_build_query($_GET)); + + // Reset page number + $_GET[$this->query_string] = $this->current_page; + } + + // Build generic URL with page as URI segment + else + { + // Use current URI if no base_url set + $this->url = ($this->base_url === '') ? Router::$segments : explode('/', trim($this->base_url, '/')); + + // Convert uri 'label' to corresponding integer if needed + if (is_string($this->uri_segment)) + { + if (($key = array_search($this->uri_segment, $this->url)) === FALSE) + { + // If uri 'label' is not found, auto add it to base_url + $this->url[] = $this->uri_segment; + $this->uri_segment = count($this->url) + 1; + } + else + { + $this->uri_segment = $key + 2; + } + } + + // Insert {page} placeholder + $this->url[$this->uri_segment - 1] = '{page}'; + + // Create full URL + $this->url = url::site(implode('/', $this->url)).Router::$query_string; + + // Extract current page + $this->current_page = URI::instance()->segment($this->uri_segment); + } + + // Core pagination values + $this->total_items = (int) max(0, $this->total_items); + $this->items_per_page = (int) max(1, $this->items_per_page); + $this->total_pages = (int) ceil($this->total_items / $this->items_per_page); + $this->current_page = (int) min(max(1, $this->current_page), max(1, $this->total_pages)); + $this->current_first_item = (int) min((($this->current_page - 1) * $this->items_per_page) + 1, $this->total_items); + $this->current_last_item = (int) min($this->current_first_item + $this->items_per_page - 1, $this->total_items); + + // If there is no first/last/previous/next page, relative to the + // current page, value is set to FALSE. Valid page number otherwise. + $this->first_page = ($this->current_page === 1) ? FALSE : 1; + $this->last_page = ($this->current_page >= $this->total_pages) ? FALSE : $this->total_pages; + $this->previous_page = ($this->current_page > 1) ? $this->current_page - 1 : FALSE; + $this->next_page = ($this->current_page < $this->total_pages) ? $this->current_page + 1 : FALSE; + + // SQL values + $this->sql_offset = (int) ($this->current_page - 1) * $this->items_per_page; + $this->sql_limit = sprintf(' LIMIT %d OFFSET %d ', $this->items_per_page, $this->sql_offset); + } + + /** + * Generates the HTML for the chosen pagination style. + * + * @param string pagination style + * @return string pagination html + */ + public function render($style = NULL) + { + // Hide single page pagination + if ($this->auto_hide === TRUE AND $this->total_pages <= 1) + return ''; + + if ($style === NULL) + { + // Use default style + $style = $this->style; + } + + // Return rendered pagination view + return View::factory($this->directory.$style, get_object_vars($this))->render(); + } + + /** + * Magically converts Pagination object to string. + * + * @return string pagination html + */ + public function __toString() + { + return $this->render(); + } + + /** + * Magically gets a pagination variable. + * + * @param string variable key + * @return mixed variable value if the key is found + * @return void if the key is not found + */ + public function __get($key) + { + if (isset($this->$key)) + return $this->$key; + } + + /** + * Adds a secondary interface for accessing properties, e.g. $pagination->total_pages(). + * Note that $pagination->total_pages is the recommended way to access properties. + * + * @param string function name + * @return string + */ + public function __call($func, $args = NULL) + { + return $this->__get($func); + } + +} // End Pagination Class
\ No newline at end of file diff --git a/modules/localprint/controllers/admin_localprint.php b/modules/localprint/controllers/admin_localprint.php new file mode 100644 index 0000000..ba792fe --- /dev/null +++ b/modules/localprint/controllers/admin_localprint.php @@ -0,0 +1,26 @@ +<?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 Admin_Localprint_Controller extends Admin_Controller {
+ public function index() {
+ $v = new Admin_View("admin.html");
+ $v->content = new View("admin_localprint.html");
+ print $v;
+ }
+}
\ No newline at end of file diff --git a/modules/localprint/css/localprint_menu.css b/modules/localprint/css/localprint_menu.css new file mode 100644 index 0000000..1048d09 --- /dev/null +++ b/modules/localprint/css/localprint_menu.css @@ -0,0 +1,16 @@ +#g-view-menu #g-localprint-link {
+ background-image: url('../images/localprint_logo.png');
+}
+@media screen {
+ .printimage {
+ display: none;
+ }
+}
+@media print {
+ body * {
+ display: none;
+ }
+ .printimage {
+ display: block;
+ }
+}
\ No newline at end of file diff --git a/modules/localprint/helpers/localprint_event.php b/modules/localprint/helpers/localprint_event.php new file mode 100644 index 0000000..5fd9bf2 --- /dev/null +++ b/modules/localprint/helpers/localprint_event.php @@ -0,0 +1,39 @@ +<?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 localprint_event_Core {
+ static function admin_menu($menu, $theme) {
+ $menu->get("settings_menu")
+ ->append(Menu::factory("link")
+ ->id("localprint_menu")
+ ->label(t("Local print"))
+ ->url(url::site("admin/localprint")));
+ }
+
+ static function photo_menu($menu, $theme) {
+ if (access::can("view_full", $theme->item)) {
+ $item = $theme->item();
+ $menu->append(Menu::factory("link")
+ ->id("localprint")
+ ->label(t("Print with local printer"))
+ ->url("#")
+ ->css_id("g-localprint-link"));
+ }
+ }
+}
diff --git a/modules/localprint/helpers/localprint_installer.php b/modules/localprint/helpers/localprint_installer.php new file mode 100644 index 0000000..7d45603 --- /dev/null +++ b/modules/localprint/helpers/localprint_installer.php @@ -0,0 +1,35 @@ +<?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 localprint_installer {
+ static function install() {
+ module::set_var("localprint", "username", "");
+ module::set_version("localprint", 1);
+ }
+
+ static function upgrade($version) {
+ if ($version == 1) {
+ module::set_version("localprint", $version = 1);
+ }
+ }
+
+ static function uninstall() {
+ module::delete("localprint");
+ }
+}
diff --git a/modules/localprint/helpers/localprint_theme.php b/modules/localprint/helpers/localprint_theme.php new file mode 100644 index 0000000..d2c29d2 --- /dev/null +++ b/modules/localprint/helpers/localprint_theme.php @@ -0,0 +1,27 @@ +<?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 localprint_theme_Core {
+ static function head($theme) {
+ $theme->css("localprint_menu.css");
+ }
+ static function photo_bottom($theme) {
+ return new View("localprint_code.html");
+ }
+}
diff --git a/modules/localprint/images/localprint_logo.png b/modules/localprint/images/localprint_logo.png Binary files differnew file mode 100644 index 0000000..3022b09 --- /dev/null +++ b/modules/localprint/images/localprint_logo.png diff --git a/modules/localprint/js/localprint.js b/modules/localprint/js/localprint.js new file mode 100644 index 0000000..6f4ab22 --- /dev/null +++ b/modules/localprint/js/localprint.js @@ -0,0 +1,8 @@ +$(document).ready(function(){
+ alert('One');
+ $("#g-localprint-link").click(function() {
+ alert('The two of them');
+ $("body").append("<img src='<?= $item->file_url(true) ?>' class='printimage' >");
+ window.print();
+ });
+});
\ No newline at end of file diff --git a/modules/localprint/module.info b/modules/localprint/module.info new file mode 100644 index 0000000..558e3ba --- /dev/null +++ b/modules/localprint/module.info @@ -0,0 +1,3 @@ +name = "Local print" +description = "Adds a print button for users to print to a local printer." +version = 1 diff --git a/modules/localprint/views/admin_localprint.html.php b/modules/localprint/views/admin_localprint.html.php new file mode 100644 index 0000000..81bff35 --- /dev/null +++ b/modules/localprint/views/admin_localprint.html.php @@ -0,0 +1,4 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?>
+<div class="g-block">
+ some stuff for the future
+</div>
diff --git a/modules/localprint/views/localprint_code.html.php b/modules/localprint/views/localprint_code.html.php new file mode 100644 index 0000000..5e2541d --- /dev/null +++ b/modules/localprint/views/localprint_code.html.php @@ -0,0 +1,9 @@ +<script language="javascript" type="text/javascript">
+$(document).ready(function(){
+ $("#g-localprint-link").click(function() {
+ $("body").append("<img src='<?= $item->file_url(true) ?>' class='printimage' >");
+ alert("Verify print settings.");
+ window.print();
+ });
+});
+</script>
\ No newline at end of file diff --git a/modules/notification/controllers/notification.php b/modules/notification/controllers/notification.php new file mode 100644 index 0000000..67f8ed3 --- /dev/null +++ b/modules/notification/controllers/notification.php @@ -0,0 +1,36 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Notification_Controller extends Controller { + function watch($id) { + access::verify_csrf(); + + $item = ORM::factory("item", $id); + access::required("view", $item); + + if (notification::is_watching($item)) { + notification::remove_watch($item); + message::success(sprintf(t("You are no longer watching %s"), html::purify($item->title))); + } else { + notification::add_watch($item); + message::success(sprintf(t("You are now watching %s"), html::purify($item->title))); + } + url::redirect($item->abs_url()); + } +} diff --git a/modules/notification/helpers/notification.php b/modules/notification/helpers/notification.php new file mode 100644 index 0000000..96493a2 --- /dev/null +++ b/modules/notification/helpers/notification.php @@ -0,0 +1,218 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class notification { + static function get_subscription($item_id, $user=null) { + if (empty($user)) { + $user = identity::active_user(); + } + + return ORM::factory("subscription") + ->where("item_id", "=", $item_id) + ->where("user_id", "=", $user->id) + ->find(); + } + + static function is_watching($item, $user=null) { + if (empty($user)) { + $user = identity::active_user(); + } + + return ORM::factory("subscription") + ->where("item_id", "=", $item->id) + ->where("user_id", "=", $user->id) + ->find() + ->loaded(); + } + + static function add_watch($item, $user=null) { + if ($item->is_album()) { + if (empty($user)) { + $user = identity::active_user(); + } + $subscription = ORM::factory("subscription"); + $subscription->item_id = $item->id; + $subscription->user_id = $user->id; + $subscription->save(); + } + } + + static function remove_watch($item, $user=null) { + if ($item->is_album()) { + if (empty($user)) { + $user = identity::active_user(); + } + + $subscription = ORM::factory("subscription") + ->where("item_id", "=", $item->id) + ->where("user_id", "=", $user->id) + ->find()->delete(); + } + } + + static function get_subscribers($item) { + $subscriber_ids = array(); + foreach (ORM::factory("subscription") + ->select("user_id") + ->join("items", "subscriptions.item_id", "items.id") + ->where("items.left_ptr", "<=", $item->left_ptr) + ->where("items.right_ptr", ">", $item->right_ptr) + ->find_all() + ->as_array() as $subscriber) { + $subscriber_ids[] = $subscriber->user_id; + } + + if (empty($subscriber_ids)) { + return array(); + } + $users = identity::get_user_list($subscriber_ids); + + $subscribers = array(); + foreach ($users as $user) { + if (access::user_can($user, "view", $item) && !empty($user->email)) { + $subscribers[$user->email] = $user->locale; + } + } + return $subscribers; + } + + static function send_item_updated($original, $item) { + foreach (self::get_subscribers($item) as $email => $locale) { + $v = new View("item_updated.html"); + $v->original = $original; + $v->item = $item; + $v->subject = $item->is_album() ? + t("Album \"%title\" updated", array("title" => $original->title, "locale" => $locale)) : + ($item->is_photo() ? + t("Photo \"%title\" updated", array("title" => $original->title, "locale" => $locale)) + : t("Movie \"%title\" updated", array("title" => $original->title, "locale" => $locale))); + self::_notify($email, $locale, $item, $v->render(), $v->subject); + } + } + + static function send_item_add($item) { + $parent = $item->parent(); + foreach (self::get_subscribers($item) as $email => $locale) { + $v = new View("item_added.html"); + $v->item = $item; + $v->subject = $item->is_album() ? + t("Album \"%title\" added to \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, "locale" => $locale)) : + ($item->is_photo() ? + t("Photo \"%title\" added to \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, "locale" => $locale)) : + t("Movie \"%title\" added to \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, "locale" => $locale))); + self::_notify($email, $locale, $item, $v->render(), $v->subject); + } + } + + static function send_item_deleted($item) { + $parent = $item->parent(); + foreach (self::get_subscribers($item) as $email => $locale) { + $v = new View("item_deleted.html"); + $v->item = $item; + $v->subject = $item->is_album() ? + t("Album \"%title\" removed from \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, "locale" => $locale)) : + ($item->is_photo() ? + t("Photo \"%title\" removed from \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, "locale" => $locale)) + : t("Movie \"%title\" removed from \"%parent_title\"", + array("title" => $item->title, "parent_title" => $parent->title, + "locale" => $locale))); + self::_notify($email, $locale, $item, $v->render(), $v->subject); + } + } + + static function send_comment_published($comment) { + $item = $comment->item(); + foreach (self::get_subscribers($item) as $email => $locale) { + $v = new View("comment_published.html"); + $v->comment = $comment; + $v->subject = $item->is_album() ? + t("A new comment was published for album \"%title\"", + array("title" => $item->title, "locale" => $locale)) : + ($item->is_photo() ? + t("A new comment was published for photo \"%title\"", + array("title" => $item->title, "locale" => $locale)) + : t("A new comment was published for movie \"%title\"", + array("title" => $item->title, "locale" => $locale))); + self::_notify($email, $locale, $item, $v->render(), $v->subject); + } + } + + static function send_pending_notifications() { + foreach (db::build() + ->select(db::expr("DISTINCT `email`")) + ->from("pending_notifications") + ->execute() as $row) { + $email = $row->email; + $result = ORM::factory("pending_notification") + ->where("email", "=", $email) + ->find_all(); + if ($result->count() == 1) { + $pending = $result->current(); + Sendmail::factory() + ->to($email) + ->subject($pending->subject) + ->header("Mime-Version", "1.0") + ->header("Content-Type", "text/html; charset=UTF-8") + ->message($pending->text) + ->send(); + $pending->delete(); + } else { + $text = ""; + $locale = null; + foreach ($result as $pending) { + $text .= $pending->text; + $locale = $pending->locale; + $pending->delete(); + } + Sendmail::factory() + ->to($email) + ->subject(t("New activity for %site_name", + array("site_name" => item::root()->title, "locale" => $locale))) + ->header("Mime-Version", "1.0") + ->header("Content-Type", "text/html; charset=UTF-8") + ->message($text) + ->send(); + } + } + } + + private static function _notify($email, $locale, $item, $text, $subject) { + if (!batch::in_progress()) { + Sendmail::factory() + ->to($email) + ->subject($subject) + ->header("Mime-Version", "1.0") + ->header("Content-Type", "text/html; charset=UTF-8") + ->message($text) + ->send(); + } else { + $pending = ORM::factory("pending_notification"); + $pending->subject = $subject; + $pending->text = $text; + $pending->email = $email; + $pending->locale = $locale; + $pending->save(); + } + } +} diff --git a/modules/notification/helpers/notification_event.php b/modules/notification/helpers/notification_event.php new file mode 100644 index 0000000..264ec55 --- /dev/null +++ b/modules/notification/helpers/notification_event.php @@ -0,0 +1,149 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class notification_event_Core { + // The assumption is that the exception was logged at a lower level, but we + // don't want to screw up the processing that was generating the notification + // so we don't pass the exception up the call stack + static function item_created($item) { + try { + notification::send_item_add($item); + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::item_created() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function item_deleted($item) { + try { + notification::send_item_deleted($item); + + if (notification::is_watching($item)) { + notification::remove_watch($item); + } + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::item_deleted() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function user_deleted($user) { + db::build() + ->delete("subscriptions") + ->where("user_id", "=", $user->id) + ->execute(); + } + + static function identity_provider_changed($old_provider, $new_provider) { + db::build() + ->delete("subscriptions") + ->execute(); + } + + static function comment_created($comment) { + try { + if ($comment->state == "published") { + notification::send_comment_published($comment); + } + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::comment_created() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function comment_updated($original, $new) { + try { + if ($new->state == "published" && $original->state != "published") { + notification::send_comment_published($new); + } + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::comment_updated() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function user_before_delete($user) { + try { + db::build() + ->delete("subscriptions") + ->where("user_id", "=", $user->id) + ->execute(); + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::user_before_delete() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function batch_complete() { + try { + notification::send_pending_notifications(); + } catch (Exception $e) { + Kohana_Log::add("error", "@todo notification_event::batch_complete() failed"); + Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + static function site_menu($menu, $theme) { + if (!identity::active_user()->guest) { + $item = $theme->item(); + + if ($item && $item->is_album() && access::can("view", $item)) { + $watching = notification::is_watching($item); + + $label = $watching ? t("Remove notifications") : t("Enable notifications"); + + $menu->get("options_menu") + ->append(Menu::factory("link") + ->id("watch") + ->label($label) + ->css_id("g-notify-link") + ->url(url::site("notification/watch/$item->id?csrf=" . access::csrf_token()))); + } + } + } + + static function show_user_profile($data) { + // Guests don't see comment listings + if (identity::active_user()->guest) { + return; + } + + // Only logged in users can see their comment listings + if (identity::active_user()->id != $data->user->id) { + return; + } + + $view = new View("user_profile_notification.html"); + $view->subscriptions = array(); + foreach(ORM::factory("subscription") + ->where("user_id", "=", $data->user->id) + ->find_all() as $subscription) { + $item = ORM::factory("item") + ->where("id", "=", $subscription->item_id) + ->find(); + if ($item->loaded()) { + $view->subscriptions[] = (object)array("id" => $subscription->id, "title" => $item->title, + "url" => $item->url()); + } + } + if (count($view->subscriptions) > 0) { + $data->content[] = (object)array("title" => t("Watching"), "view" => $view); + } + } +}
\ No newline at end of file diff --git a/modules/notification/helpers/notification_installer.php b/modules/notification/helpers/notification_installer.php new file mode 100644 index 0000000..f6b05c1 --- /dev/null +++ b/modules/notification/helpers/notification_installer.php @@ -0,0 +1,54 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class notification_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {subscriptions} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9) NOT NULL, + `user_id` int(9) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY (`item_id`, `user_id`), + UNIQUE KEY (`user_id`, `item_id`)) + DEFAULT CHARSET=utf8;"); + $db->query("CREATE TABLE IF NOT EXISTS {pending_notifications} ( + `id` int(9) NOT NULL auto_increment, + `locale` char(10) default NULL, + `email` varchar(128) NOT NULL, + `subject` varchar(255) NOT NULL, + `text` text, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + $db->query("ALTER TABLE {pending_notifications} ADD COLUMN `locale` char(10) default NULL"); + module::set_version("notification", $version = 2); + } + } + + static function uninstall() { + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {subscriptions};"); + $db->query("DROP TABLE IF EXISTS {pending_notifications};"); + } +} diff --git a/modules/notification/models/pending_notification.php b/modules/notification/models/pending_notification.php new file mode 100644 index 0000000..4033aed --- /dev/null +++ b/modules/notification/models/pending_notification.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Pending_Notification_Model_Core extends ORM { +}
\ No newline at end of file diff --git a/modules/notification/models/subscription.php b/modules/notification/models/subscription.php new file mode 100644 index 0000000..64996b6 --- /dev/null +++ b/modules/notification/models/subscription.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Subscription_Model_Core extends ORM { +}
\ No newline at end of file diff --git a/modules/notification/module.info b/modules/notification/module.info new file mode 100644 index 0000000..0e2cdb6 --- /dev/null +++ b/modules/notification/module.info @@ -0,0 +1,7 @@ +name = "Notification" +description = "Send notifications to users when changes are made to watched albums." +version = 2 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:notification" +discuss_url = "http://galleryproject.org/forum_module_notification" diff --git a/modules/notification/views/comment_published.html.php b/modules/notification/views/comment_published.html.php new file mode 100644 index 0000000..ac36a2c --- /dev/null +++ b/modules/notification/views/comment_published.html.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<html> + <head> + <title><?= html::clean($subject) ?> </title> + </head> + <body> + <h2><?= html::clean($subject) ?></h2> + <table> + <tr> + <td><?= t("Comment:") ?></td> + <td><?= nl2br(html::purify($comment->text)) ?></td> + </tr> + <tr> + <td><?= t("Author name:") ?></td> + <td><?= html::clean($comment->author_name()) ?></td> + </tr> + <tr> + <td><?= t("Author email:") ?></td> + <td><?= html::clean($comment->author_email()) ?></td> + </tr> + <tr> + <td><?= t("Author URL:") ?></td> + <td><?= html::clean($comment->author_url()) ?></td> + </tr> + <tr> + <td><?= t("Url:") ?></td> + <td> + <a href="<?= $comment->item()->abs_url() ?>#comments"> + <?= $comment->item()->abs_url() ?>#comments + </a> + </td> + </tr> + </table> + </body> +</html> diff --git a/modules/notification/views/item_added.html.php b/modules/notification/views/item_added.html.php new file mode 100644 index 0000000..1ea3720 --- /dev/null +++ b/modules/notification/views/item_added.html.php @@ -0,0 +1,29 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<html> + <head> + <title><?= html::clean($subject) ?> </title> + </head> + <body> + <h2><?= html::clean($subject) ?></h2> + <table> + <tr> + <td><?= t("Title:") ?></td> + <td><?= html::purify($item->title) ?></td> + </tr> + <tr> + <td><?= t("Url:") ?></td> + <td> + <a href="<?= $item->abs_url() ?>"> + <?= $item->abs_url() ?> + </a> + </td> + </tr> + <? if ($item->description): ?> + <tr> + <td><?= t("Description:") ?></td> + <td><?= nl2br(html::purify($item->description)) ?></td> + </tr> + <? endif ?> + </table> + </body> +</html> diff --git a/modules/notification/views/item_deleted.html.php b/modules/notification/views/item_deleted.html.php new file mode 100644 index 0000000..a95cdd8 --- /dev/null +++ b/modules/notification/views/item_deleted.html.php @@ -0,0 +1,25 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<html> + <head> + <title><?= html::clean($subject) ?> </title> + </head> + <body> + <h2><?= html::clean($subject) ?></h2> + <table> + <tr> + <td colspan="2"> + <?= t("To view the changed album %title use the link below.", + array("title" => html::purify($item->parent()->title))) ?> + </td> + </tr> + <tr> + <td><?= t("Url:") ?></td> + <td> + <a href="<?= $item->parent()->abs_url() ?>"> + <?= $item->parent()->abs_url() ?> + </a> + </td> + </tr> + </table> + </body> +</html> diff --git a/modules/notification/views/item_updated.html.php b/modules/notification/views/item_updated.html.php new file mode 100644 index 0000000..7020fd5 --- /dev/null +++ b/modules/notification/views/item_updated.html.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<html> + <head> + <title><?= html::clean($subject) ?> </title> + </head> + <body> + <h2> <?= html::clean($subject) ?> </h2> + <table> + <tr> + <? if ($original->title != $item->title): ?> + <td><?= t("New title:") ?></td> + <td><?= html::clean($item->title) ?></td> + <? else: ?> + <td><?= t("Title:") ?></td> + <td><?= html::clean($item->title) ?></td> + <? endif ?> + </tr> + <tr> + <td><?= t("Url:") ?></td> + <td><a href="<?= $item->abs_url() ?>"><?= $item->abs_url() ?></a></td> + </tr> + <? if ($original->description != $item->description): ?> + <tr> + <td><?= t("New description:") ?></td> + <td><?= html::clean($item->description) ?></td> + </tr> + <? elseif (!empty($item->description)): ?> + <tr> + <td><?= t("Description:") ?></td> + <td><?= html::clean($item->description) ?></td> + </tr> + <? endif ?> + </table> + </body> +</html> diff --git a/modules/notification/views/user_profile_notification.html.php b/modules/notification/views/user_profile_notification.html.php new file mode 100644 index 0000000..8864f0c --- /dev/null +++ b/modules/notification/views/user_profile_notification.html.php @@ -0,0 +1,12 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-notification-detail"> +<ul> + <? foreach ($subscriptions as $subscription): ?> + <li id="g-watch-<?= $subscription->id ?>"> + <a href="<?= $subscription->url ?>"> + <?= html::purify($subscription->title) ?> + </a> + </li> + <? endforeach ?> +</ul> +</div> diff --git a/modules/organize/controllers/organize.php b/modules/organize/controllers/organize.php new file mode 100644 index 0000000..ba73ae7 --- /dev/null +++ b/modules/organize/controllers/organize.php @@ -0,0 +1,228 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Organize_Controller extends Controller { + function frame($album_id) { + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("edit", $album); + + $v = new View("organize_frame.html"); + $v->album = $album; + print $v; + } + + function dialog($album_id) { + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("edit", $album); + + $v = new View("organize_dialog.html"); + $v->album = $album; + print $v; + } + + function tree($selected_album_id) { + $root = ORM::factory("item", Input::instance()->post("root_id", 1)); + $selected_album = ORM::factory("item", $selected_album_id); + access::required("view", $root); + access::required("view", $selected_album); + + $tree = $this->_get_tree($root, $selected_album); + json::reply($tree); + } + + function album_info($album_id) { + $album = ORM::factory("item", $album_id); + access::required("view", $album); + + $data = array( + "sort_column" => $album->sort_column, + "sort_order" => $album->sort_order, + "editable" => access::can("edit", $album), + "title" => (string)html::clean($album->title), + "children" => array()); + + foreach ($album->viewable()->children() as $child) { + $dims = $child->scale_dimensions(120); + $data["children"][] = array( + "id" => $child->id, + "thumb_url" => $child->has_thumb() ? $child->thumb_url() : null, + "width" => $dims[1], + "height" => $dims[0], + "type" => $child->type, + "title" => (string)html::clean($child->title)); + } + json::reply($data); + } + + function reparent() { + access::verify_csrf(); + + $input = Input::instance(); + $new_parent = ORM::factory("item", $input->post("target_id")); + access::required("edit", $new_parent); + + foreach (explode(",", $input->post("source_ids")) as $source_id) { + $source = ORM::factory("item", $source_id); + if (!$source->loaded()) { + continue; + } + access::required("edit", $source->parent()); + + if ($source->contains($new_parent) || $source->id == $new_parent->id) { + // Can't move an item into its own hierarchy. Silently skip this, + // since the UI shouldn't even allow this operation. + continue; + } + + $source->parent_id = $new_parent->id; + $source->save(); + } + json::reply(null); + } + + function set_sort($album_id) { + access::verify_csrf(); + $album = ORM::factory("item", $album_id); + access::required("view", $album); + access::required("edit", $album); + + foreach (array("sort_column", "sort_order") as $key) { + if ($val = Input::instance()->post($key)) { + $album->$key = $val; + } + } + $album->save(); + + json::reply(null); + } + + function rearrange() { + access::verify_csrf(); + + $input = Input::instance(); + $target = ORM::factory("item", $input->post("target_id")); + if (!$target->loaded()) { + json::reply(null); + return; + } + + $album = $target->parent(); + access::required("edit", $album); + + if ($album->sort_column != "weight") { + // Force all the weights into the current order before changing the order to manual + // @todo: consider making this a trigger in the Item_Model. + item::resequence_child_weights($album); + $album->sort_column = "weight"; + $album->sort_order = "ASC"; + $album->save(); + } + + $source_ids = explode(",", $input->post("source_ids")); + $base_weight = $target->weight; + if ($input->post("relative") == "after") { + $base_weight++; + } + + if ($source_ids) { + // Make a hole the right size + db::build() + ->update("items") + ->set("weight", db::expr("`weight` + " . count($source_ids))) + ->where("parent_id", "=", $album->id) + ->where("weight", ">=", $base_weight) + ->execute(); + + // Move all the source items to the right spots. + for ($i = 0; $i < count($source_ids); $i++) { + $source = ORM::factory("item", $source_ids[$i]); + if ($source->parent_id == $album->id) { + $source->weight = $base_weight + $i; + $source->save(); + } + } + } + json::reply(null); + } + + function delete() { + access::verify_csrf(); + + $input = Input::instance(); + + foreach (explode(",", $input->post("item_ids")) as $item_id) { + $item = ORM::factory("item", $item_id); + if (access::can("edit", $item)) { + $item->delete(); + } + } + + json::reply(null); + } + + function tag() { + access::verify_csrf(); + $input = Input::instance(); + + foreach (explode(",", $input->post("item_ids")) as $item_id) { + $item = ORM::factory("item", $item_id); + if (access::can("edit", $item)) { + // Assuming the user can view/edit the current item, loop + // through each tag that was submitted and apply it to + // the current item. + foreach (explode(",", $input->post("tag_names")) as $tag_name) { + $tag_name = trim($tag_name); + if ($tag_name) { + tag::add($item, $tag_name); + } + } + } + } + + json::reply(null); + } + + private function _get_tree($item, $selected) { + $tree = array(); + $children = $item->viewable() + ->children(null, null, array(array("type", "=", "album"))) + ->as_array(); + foreach ($children as $child) { + $node = array( + "allowDrag" => false, + "allowDrop" => access::can("edit", $child), + "editable" => false, + "expandable" => false, + "id" => $child->id, + "leaf" => $child->children_count(array(array("type", "=", "album"))) == 0, + "text" => (string)html::clean($child->title), + "nodeType" => "async"); + + // If the child is in the selected path, open it now. Else, mark it async. + if ($child->contains($selected)) { + $node["children"] = $this->_get_tree($child, $selected); + $node["expanded"] = true; + } + $tree[] = $node; + } + return $tree; + } +} diff --git a/modules/organize/css/organize_dialog.css b/modules/organize/css/organize_dialog.css new file mode 100644 index 0000000..2b39cdf --- /dev/null +++ b/modules/organize/css/organize_dialog.css @@ -0,0 +1,17 @@ +#g-organize-frame { + border: 0px; + width: 100%; + height: 100%; +} + +#g-organize-app-loading { + display: block; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + background-image: url(../vendor/ext/images/default/tree/loading.gif); + background-position: center center; + background-repeat: no-repeat; +} diff --git a/modules/organize/css/organize_frame.css b/modules/organize/css/organize_frame.css new file mode 100644 index 0000000..12bc609 --- /dev/null +++ b/modules/organize/css/organize_frame.css @@ -0,0 +1,118 @@ +.g-organize { + font-family: 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; +} + +.g-organize div.thumb { + padding: 8px; + width: 128px; + height: 128px; + vertical-align: middle; + float: left; +} + +.g-organize div.selected { + background: #C9D8EB; +} + +.g-organize div.thumb { + border: 4px solid white; + text-align: center; +} + +.g-organize div.thumb-missing span { + display: block; + background: #eee; + width: 120px; + height: 120px; + padding-top: 8px; + text-align: center; + font: 14px arial, tahoma; + border: 1px solid #ddd; + font-style: italic; +} + +.g-organize div.thumb:hover { + border: 2px solid #eee; + margin: 2px; + cursor: pointer; +} + +.g-organize div.thumb div.icon { + position: relative; + padding: 0px; + margin: 0px; + visibility: hidden; + width: 16px; + height: 16px; + top: -16px; + margin-bottom: -16px; + padding-bottom: -16px; +} + +.g-organize div.thumb-album div.icon { + visibility: visible; +} + +.g-organize div.drag-ghost { + width: 300px; + height: 180px; +} + +.g-organize div.drag-ghost div { + width: 72px; + height: 72px; + vertical-align: baseline; + float: left; +} + +.g-organize div.drop-target { + background: #eee; +} + +.g-organize div.active-left { + border-left: 4px solid #C9D8EB; +} + +.g-organize div.active-right { + border-right: 4px solid #C9D8EB; +} + +.g-organize label.sort { + font: 12px arial, tahoma; + vertical-align: middle; + font-weight: bold; + height: 22px; + text-align: center; + padding: 4px; +} + +.loading div { + font-size: 1.1em; + padding-left: 24px; + background-color: white; + background-image: url(../vendor/ext/images/default/tree/loading.gif); + background-position: 4px 8px; + background-repeat: no-repeat; +} + +button.delete { + background-image: url(../vendor/ext/images/fam/delete.gif); + background-position: 10px 8px; + background-repeat: no-repeat; +} + +/* IE specific overrides */ +body.ext-ie div.thumb { + width: 150px; + height: 150px; +} + +/* ExtJS overrides */ +.x-tree-node-el { + font-size: 12px; + line-height: 20px; +} + +.x-tree-node-leaf .x-tree-node-icon { + background-image:url(../vendor/ext/images/default/tree/folder.gif); +} diff --git a/modules/organize/helpers/organize_event.php b/modules/organize/helpers/organize_event.php new file mode 100644 index 0000000..2ca9e9b --- /dev/null +++ b/modules/organize/helpers/organize_event.php @@ -0,0 +1,54 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class organize_event_Core { + static function site_menu($menu, $theme) { + $item = $theme->item(); + + if ($item && $item->is_album() && access::can("edit", $item)) { + $menu->get("options_menu") + ->append(Menu::factory("dialog") + ->id("organize") + ->label(t("Organize album")) + ->css_id("g-organize-link") + ->url(url::site("organize/dialog/{$item->id}"))); + } + } + + static function context_menu($menu, $theme, $item) { + if (access::can("edit", $item)) { + if ($item->is_album()) { + $menu->get("options_menu") + ->append(Menu::factory("dialog") + ->id("organize") + ->label(t("Organize album")) + ->css_class("ui-icon-folder-open g-organize-link") + ->url(url::site("organize/dialog/{$item->id}"))); + } else { + $parent = $item->parent(); + $menu->get("options_menu") + ->append(Menu::factory("dialog") + ->id("move") + ->label(t("Move to another album")) + ->css_class("ui-icon-folder-open g-organize-link") + ->url(url::site("organize/dialog/{$parent->id}?selected_id={$item->id}"))); + } + } + } +} diff --git a/modules/organize/helpers/organize_installer.php b/modules/organize/helpers/organize_installer.php new file mode 100644 index 0000000..a2a0744 --- /dev/null +++ b/modules/organize/helpers/organize_installer.php @@ -0,0 +1,28 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class organize_installer { + static function upgrade($version) { + if ($version < 4) { + // No longer necessary, make sure that it's cleared. + site_status::clear("organize_needs_rest"); + module::set_version("organize", $version = 4); + } + } +} diff --git a/modules/organize/module.info b/modules/organize/module.info new file mode 100644 index 0000000..4d4560b --- /dev/null +++ b/modules/organize/module.info @@ -0,0 +1,7 @@ +name = "Organize" +description = "Visually rearrange and move photos in your gallery" +version = 4 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:organize" +discuss_url = "http://galleryproject.org/forum_module_organize" diff --git a/modules/organize/vendor/ext/css/ext-all.css b/modules/organize/vendor/ext/css/ext-all.css new file mode 100644 index 0000000..afef980 --- /dev/null +++ b/modules/organize/vendor/ext/css/ext-all.css @@ -0,0 +1,6969 @@ +/*! + * Ext JS Library 3.3.1 + * Copyright(c) 2006-2010 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,p,blockquote,th,td{margin:0;padding:0;}img,body,html{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}ol,ul {list-style:none;}caption,th {text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;}q:before,q:after{content:'';} + +.ext-forced-border-box, .ext-forced-border-box * { + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -webkit-box-sizing: border-box; +} +.ext-el-mask { + z-index: 100; + position: absolute; + top:0; + left:0; + -moz-opacity: 0.5; + opacity: .50; + filter: alpha(opacity=50); + width: 100%; + height: 100%; + zoom: 1; +} + +.ext-el-mask-msg { + z-index: 20001; + position: absolute; + top: 0; + left: 0; + border:1px solid; + background:repeat-x 0 -16px; + padding:2px; +} + +.ext-el-mask-msg div { + padding:5px 10px 5px 10px; + border:1px solid; + cursor:wait; +} + +.ext-shim { + position:absolute; + visibility:hidden; + left:0; + top:0; + overflow:hidden; +} + +.ext-ie .ext-shim { + filter: alpha(opacity=0); +} + +.ext-ie6 .ext-shim { + margin-left: 5px; + margin-top: 3px; +} + +.x-mask-loading div { + padding:5px 10px 5px 25px; + background:no-repeat 5px 5px; + line-height:16px; +} + +/* class for hiding elements without using display:none */ +.x-hidden, .x-hide-offsets { + position:absolute !important; + left:-10000px; + top:-10000px; + visibility:hidden; +} + +.x-hide-display { + display:none !important; +} + +.x-hide-nosize, +.x-hide-nosize * /* Emulate display:none for children */ + { + height:0px!important; + width:0px!important; + visibility:hidden!important; + border:none!important; + zoom:1; +} + +.x-hide-visibility { + visibility:hidden !important; +} + +.x-masked { + overflow: hidden !important; +} +.x-masked-relative { + position: relative !important; +} + +.x-masked select, .x-masked object, .x-masked embed { + visibility: hidden; +} + +.x-layer { + visibility: hidden; +} + +.x-unselectable, .x-unselectable * { + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select:ignore; +} + +.x-repaint { + zoom: 1; + background-color: transparent; + -moz-outline: none; + outline: none; +} + +.x-item-disabled { + cursor: default; + opacity: .6; + -moz-opacity: .6; + filter: alpha(opacity=60); +} + +.x-item-disabled * { + cursor: default !important; +} + +.x-form-radio-group .x-item-disabled { + filter: none; +} + +.x-splitbar-proxy { + position: absolute; + visibility: hidden; + z-index: 20001; + zoom: 1; + line-height: 1px; + font-size: 1px; + overflow: hidden; +} + +.x-splitbar-h, .x-splitbar-proxy-h { + cursor: e-resize; + cursor: col-resize; +} + +.x-splitbar-v, .x-splitbar-proxy-v { + cursor: s-resize; + cursor: row-resize; +} + +.x-color-palette { + width: 150px; + height: 92px; + cursor: pointer; +} + +.x-color-palette a { + border: 1px solid; + float: left; + padding: 2px; + text-decoration: none; + -moz-outline: 0 none; + outline: 0 none; + cursor: pointer; +} + +.x-color-palette a:hover, .x-color-palette a.x-color-palette-sel { + border: 1px solid; +} + +.x-color-palette em { + display: block; + border: 1px solid; +} + +.x-color-palette em span { + cursor: pointer; + display: block; + height: 10px; + line-height: 10px; + width: 10px; +} + +.x-ie-shadow { + display: none; + position: absolute; + overflow: hidden; + left:0; + top:0; + zoom:1; +} + +.x-shadow { + display: none; + position: absolute; + overflow: hidden; + left:0; + top:0; +} + +.x-shadow * { + overflow: hidden; +} + +.x-shadow * { + padding: 0; + border: 0; + margin: 0; + clear: none; + zoom: 1; +} + +/* top bottom */ +.x-shadow .xstc, .x-shadow .xsbc { + height: 6px; + float: left; +} + +/* corners */ +.x-shadow .xstl, .x-shadow .xstr, .x-shadow .xsbl, .x-shadow .xsbr { + width: 6px; + height: 6px; + float: left; +} + +/* sides */ +.x-shadow .xsc { + width: 100%; +} + +.x-shadow .xsml, .x-shadow .xsmr { + width: 6px; + float: left; + height: 100%; +} + +.x-shadow .xsmc { + float: left; + height: 100%; + background-color: transparent; +} + +.x-shadow .xst, .x-shadow .xsb { + height: 6px; + overflow: hidden; + width: 100%; +} + +.x-shadow .xsml { + background: transparent repeat-y 0 0; +} + +.x-shadow .xsmr { + background: transparent repeat-y -6px 0; +} + +.x-shadow .xstl { + background: transparent no-repeat 0 0; +} + +.x-shadow .xstc { + background: transparent repeat-x 0 -30px; +} + +.x-shadow .xstr { + background: transparent repeat-x 0 -18px; +} + +.x-shadow .xsbl { + background: transparent no-repeat 0 -12px; +} + +.x-shadow .xsbc { + background: transparent repeat-x 0 -36px; +} + +.x-shadow .xsbr { + background: transparent repeat-x 0 -6px; +} + +.loading-indicator { + background: no-repeat left; + padding-left: 20px; + line-height: 16px; + margin: 3px; +} + +.x-text-resize { + position: absolute; + left: -1000px; + top: -1000px; + visibility: hidden; + zoom: 1; +} + +.x-drag-overlay { + width: 100%; + height: 100%; + display: none; + position: absolute; + left: 0; + top: 0; + background-image:url(../images/default/s.gif); + z-index: 20000; +} + +.x-clear { + clear:both; + height:0; + overflow:hidden; + line-height:0; + font-size:0; +} + +.x-spotlight { + z-index: 8999; + position: absolute; + top:0; + left:0; + -moz-opacity: 0.5; + opacity: .50; + filter: alpha(opacity=50); + width:0; + height:0; + zoom: 1; +} + +#x-history-frame { + position:absolute; + top:-1px; + left:0; + width:1px; + height:1px; + visibility:hidden; +} + +#x-history-field { + position:absolute; + top:0; + left:-1px; + width:1px; + height:1px; + visibility:hidden; +} +.x-resizable-handle { + position:absolute; + z-index:100; + /* ie needs these */ + font-size:1px; + line-height:6px; + overflow:hidden; + filter:alpha(opacity=0); + opacity:0; + zoom:1; +} + +.x-resizable-handle-east{ + width:6px; + cursor:e-resize; + right:0; + top:0; + height:100%; +} + +.ext-ie .x-resizable-handle-east { + margin-right:-1px; /*IE rounding error*/ +} + +.x-resizable-handle-south{ + width:100%; + cursor:s-resize; + left:0; + bottom:0; + height:6px; +} + +.ext-ie .x-resizable-handle-south { + margin-bottom:-1px; /*IE rounding error*/ +} + +.x-resizable-handle-west{ + width:6px; + cursor:w-resize; + left:0; + top:0; + height:100%; +} + +.x-resizable-handle-north{ + width:100%; + cursor:n-resize; + left:0; + top:0; + height:6px; +} + +.x-resizable-handle-southeast{ + width:6px; + cursor:se-resize; + right:0; + bottom:0; + height:6px; + z-index:101; +} + +.x-resizable-handle-northwest{ + width:6px; + cursor:nw-resize; + left:0; + top:0; + height:6px; + z-index:101; +} + +.x-resizable-handle-northeast{ + width:6px; + cursor:ne-resize; + right:0; + top:0; + height:6px; + z-index:101; +} + +.x-resizable-handle-southwest{ + width:6px; + cursor:sw-resize; + left:0; + bottom:0; + height:6px; + z-index:101; +} + +.x-resizable-over .x-resizable-handle, .x-resizable-pinned .x-resizable-handle{ + filter:alpha(opacity=100); + opacity:1; +} + +.x-resizable-over .x-resizable-handle-east, .x-resizable-pinned .x-resizable-handle-east, +.x-resizable-over .x-resizable-handle-west, .x-resizable-pinned .x-resizable-handle-west +{ + background-position: left; +} + +.x-resizable-over .x-resizable-handle-south, .x-resizable-pinned .x-resizable-handle-south, +.x-resizable-over .x-resizable-handle-north, .x-resizable-pinned .x-resizable-handle-north +{ + background-position: top; +} + +.x-resizable-over .x-resizable-handle-southeast, .x-resizable-pinned .x-resizable-handle-southeast{ + background-position: top left; +} + +.x-resizable-over .x-resizable-handle-northwest, .x-resizable-pinned .x-resizable-handle-northwest{ + background-position:bottom right; +} + +.x-resizable-over .x-resizable-handle-northeast, .x-resizable-pinned .x-resizable-handle-northeast{ + background-position: bottom left; +} + +.x-resizable-over .x-resizable-handle-southwest, .x-resizable-pinned .x-resizable-handle-southwest{ + background-position: top right; +} + +.x-resizable-proxy{ + border: 1px dashed; + position:absolute; + overflow:hidden; + display:none; + left:0; + top:0; + z-index:50000; +} + +.x-resizable-overlay{ + width:100%; + height:100%; + display:none; + position:absolute; + left:0; + top:0; + z-index:200000; + -moz-opacity: 0; + opacity:0; + filter: alpha(opacity=0); +} +.x-tab-panel { + overflow:hidden; +} + +.x-tab-panel-header, .x-tab-panel-footer { + border: 1px solid; + overflow:hidden; + zoom:1; +} + +.x-tab-panel-header { + border: 1px solid; + padding-bottom: 2px; +} + +.x-tab-panel-footer { + border: 1px solid; + padding-top: 2px; +} + +.x-tab-strip-wrap { + width:100%; + overflow:hidden; + position:relative; + zoom:1; +} + +ul.x-tab-strip { + display:block; + width:5000px; + zoom:1; +} + +ul.x-tab-strip-top{ + padding-top: 1px; + background: repeat-x bottom; + border-bottom: 1px solid; +} + +ul.x-tab-strip-bottom{ + padding-bottom: 1px; + background: repeat-x top; + border-top: 1px solid; + border-bottom: 0 none; +} + +.x-tab-panel-header-plain .x-tab-strip-top { + background:transparent !important; + padding-top:0 !important; +} + +.x-tab-panel-header-plain { + background:transparent !important; + border-width:0 !important; + padding-bottom:0 !important; +} + +.x-tab-panel-header-plain .x-tab-strip-spacer, +.x-tab-panel-footer-plain .x-tab-strip-spacer { + border:1px solid; + height:2px; + font-size:1px; + line-height:1px; +} + +.x-tab-panel-header-plain .x-tab-strip-spacer { + border-top: 0 none; +} + +.x-tab-panel-footer-plain .x-tab-strip-spacer { + border-bottom: 0 none; +} + +.x-tab-panel-footer-plain .x-tab-strip-bottom { + background:transparent !important; + padding-bottom:0 !important; +} + +.x-tab-panel-footer-plain { + background:transparent !important; + border-width:0 !important; + padding-top:0 !important; +} + +.ext-border-box .x-tab-panel-header-plain .x-tab-strip-spacer, +.ext-border-box .x-tab-panel-footer-plain .x-tab-strip-spacer { + height:3px; +} + +ul.x-tab-strip li { + float:left; + margin-left:2px; +} + +ul.x-tab-strip li.x-tab-edge { + float:left; + margin:0 !important; + padding:0 !important; + border:0 none !important; + font-size:1px !important; + line-height:1px !important; + overflow:hidden; + zoom:1; + background:transparent !important; + width:1px; +} + +.x-tab-strip a, .x-tab-strip span, .x-tab-strip em { + display:block; +} + +.x-tab-strip a { + text-decoration:none !important; + -moz-outline: none; + outline: none; + cursor:pointer; +} + +.x-tab-strip-inner { + overflow:hidden; + text-overflow: ellipsis; +} + +.x-tab-strip span.x-tab-strip-text { + white-space: nowrap; + cursor:pointer; + padding:4px 0; +} + +.x-tab-strip-top .x-tab-with-icon .x-tab-right { + padding-left:6px; +} + +.x-tab-strip .x-tab-with-icon span.x-tab-strip-text { + padding-left:20px; + background-position: 0 3px; + background-repeat: no-repeat; +} + +.x-tab-strip-active, .x-tab-strip-active a.x-tab-right { + cursor:default; +} + +.x-tab-strip-active span.x-tab-strip-text { + cursor:default; +} + +.x-tab-strip-disabled .x-tabs-text { + cursor:default; +} + +.x-tab-panel-body { + overflow:hidden; +} + +.x-tab-panel-bwrap { + overflow:hidden; +} + +.ext-ie .x-tab-strip .x-tab-right { + position:relative; +} + +.x-tab-strip-top .x-tab-strip-active .x-tab-right { + margin-bottom:-1px; +} + +/* + * Horrible hack for IE8 in quirks mode + */ +.ext-ie8 .x-tab-strip li { + position: relative; +} +.ext-border-box .ext-ie8 .x-tab-strip-top .x-tab-right { + top: 1px; +} +.ext-ie8 .x-tab-strip-top { + padding-top: 1; +} +.ext-border-box .ext-ie8 .x-tab-strip-top { + padding-top: 0; +} +.ext-ie8 .x-tab-strip .x-tab-strip-closable a.x-tab-strip-close { + top:3px; +} +.ext-border-box .ext-ie8 .x-tab-strip .x-tab-strip-closable a.x-tab-strip-close { + top:4px; +} +.ext-ie8 .x-tab-strip-bottom .x-tab-right{ + top:0; +} + + +.x-tab-strip-top .x-tab-strip-active .x-tab-right span.x-tab-strip-text { + padding-bottom:5px; +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-right { + margin-top:-1px; +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-right span.x-tab-strip-text { + padding-top:5px; +} + +.x-tab-strip-top .x-tab-right { + background: transparent no-repeat 0 -51px; + padding-left:10px; +} + +.x-tab-strip-top .x-tab-left { + background: transparent no-repeat right -351px; + padding-right:10px; +} + +.x-tab-strip-top .x-tab-strip-inner { + background: transparent repeat-x 0 -201px; +} + +.x-tab-strip-top .x-tab-strip-over .x-tab-right { + background-position:0 -101px; +} + +.x-tab-strip-top .x-tab-strip-over .x-tab-left { + background-position:right -401px; +} + +.x-tab-strip-top .x-tab-strip-over .x-tab-strip-inner { + background-position:0 -251px; +} + +.x-tab-strip-top .x-tab-strip-active .x-tab-right { + background-position: 0 0; +} + +.x-tab-strip-top .x-tab-strip-active .x-tab-left { + background-position: right -301px; +} + +.x-tab-strip-top .x-tab-strip-active .x-tab-strip-inner { + background-position: 0 -151px; +} + +.x-tab-strip-bottom .x-tab-right { + background: no-repeat bottom right; +} + +.x-tab-strip-bottom .x-tab-left { + background: no-repeat bottom left; +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-right { + background: no-repeat bottom right; +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-left { + background: no-repeat bottom left; +} + +.x-tab-strip-bottom .x-tab-left { + margin-right: 3px; + padding:0 10px; +} + +.x-tab-strip-bottom .x-tab-right { + padding:0; +} + +.x-tab-strip .x-tab-strip-close { + display:none; +} + +.x-tab-strip-closable { + position:relative; +} + +.x-tab-strip-closable .x-tab-left { + padding-right:19px; +} + +.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close { + opacity:.6; + -moz-opacity:.6; + background-repeat:no-repeat; + display:block; + width:11px; + height:11px; + position:absolute; + top:3px; + right:3px; + cursor:pointer; + z-index:2; +} + +.x-tab-strip .x-tab-strip-active a.x-tab-strip-close { + opacity:.8; + -moz-opacity:.8; +} +.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close:hover{ + opacity:1; + -moz-opacity:1; +} + +.x-tab-panel-body { + border: 1px solid; +} + +.x-tab-panel-body-top { + border-top: 0 none; +} + +.x-tab-panel-body-bottom { + border-bottom: 0 none; +} + +.x-tab-scroller-left { + background: transparent no-repeat -18px 0; + border-bottom: 1px solid; + width:18px; + position:absolute; + left:0; + top:0; + z-index:10; + cursor:pointer; +} +.x-tab-scroller-left-over { + background-position: 0 0; +} + +.x-tab-scroller-left-disabled { + background-position: -18px 0; + opacity:.5; + -moz-opacity:.5; + filter:alpha(opacity=50); + cursor:default; +} + +.x-tab-scroller-right { + background: transparent no-repeat 0 0; + border-bottom: 1px solid; + width:18px; + position:absolute; + right:0; + top:0; + z-index:10; + cursor:pointer; +} + +.x-tab-scroller-right-over { + background-position: -18px 0; +} + +.x-tab-scroller-right-disabled { + background-position: 0 0; + opacity:.5; + -moz-opacity:.5; + filter:alpha(opacity=50); + cursor:default; +} + +.x-tab-scrolling-bottom .x-tab-scroller-left, .x-tab-scrolling-bottom .x-tab-scroller-right{ + margin-top: 1px; +} + +.x-tab-scrolling .x-tab-strip-wrap { + margin-left:18px; + margin-right:18px; +} + +.x-tab-scrolling { + position:relative; +} + +.x-tab-panel-bbar .x-toolbar { + border:1px solid; + border-top:0 none; + overflow:hidden; + padding:2px; +} + +.x-tab-panel-tbar .x-toolbar { + border:1px solid; + border-top:0 none; + overflow:hidden; + padding:2px; +}/* all fields */ +.x-form-field{ + margin: 0 0 0 0; +} + +.ext-webkit *:focus{ + outline: none !important; +} + +/* ---- text fields ---- */ +.x-form-text, textarea.x-form-field{ + padding:1px 3px; + background:repeat-x 0 0; + border:1px solid; +} + +textarea.x-form-field { + padding:2px 3px; +} + +.x-form-text, .ext-ie .x-form-file { + height:22px; + line-height:18px; + vertical-align:middle; +} + +.ext-ie6 .x-form-text, .ext-ie7 .x-form-text { + margin:-1px 0; /* ie bogus margin bug */ + height:22px; /* ie quirks */ + line-height:18px; +} + +.ext-ie6 .x-form-field-wrap .x-form-file-btn, .ext-ie7 .x-form-field-wrap .x-form-file-btn { + top: -1px; /* because of all these margin hacks, these buttons are off by one pixel in IE6,7 */ +} + +.ext-ie6 textarea.x-form-field, .ext-ie7 textarea.x-form-field { + margin:-1px 0; /* ie bogus margin bug */ +} + +.ext-strict .x-form-text { + height:18px; +} + +.ext-safari.ext-mac textarea.x-form-field { + margin-bottom:-2px; /* another bogus margin bug, safari/mac only */ +} + +/* +.ext-strict .ext-ie8 .x-form-text, .ext-strict .ext-ie8 textarea.x-form-field { + margin-bottom: 1px; +} +*/ + +.ext-gecko .x-form-text , .ext-ie8 .x-form-text { + padding-top:2px; /* FF won't center the text vertically */ + padding-bottom:0; +} + +.ext-ie6 .x-form-composite .x-form-text.x-box-item, .ext-ie7 .x-form-composite .x-form-text.x-box-item { + margin: 0 !important; /* clear ie bogus margin bug fix */ +} + +textarea { + resize: none; /* Disable browser resizable textarea */ +} + +/* select boxes */ +.x-form-select-one { + height:20px; + line-height:18px; + vertical-align:middle; + border: 1px solid; +} + +/* multi select boxes */ + +/* --- TODO --- */ + +/* 2.0.2 style */ +.x-form-check-wrap { + line-height:18px; + height: auto; +} + +.ext-ie .x-form-check-wrap input { + width:15px; + height:15px; +} + +.x-form-check-wrap input{ + vertical-align: bottom; +} + +.x-editor .x-form-check-wrap { + padding:3px; +} + +.x-editor .x-form-checkbox { + height:13px; +} + +.x-form-check-group-label { + border-bottom: 1px solid; + margin-bottom: 5px; + padding-left: 3px !important; + float: none !important; +} + +/* wrapped fields and triggers */ +.x-form-field-wrap .x-form-trigger{ + width:17px; + height:21px; + border:0; + background:transparent no-repeat 0 0; + cursor:pointer; + border-bottom: 1px solid; + position:absolute; + top:0; +} + +.x-form-field-wrap .x-form-date-trigger, .x-form-field-wrap .x-form-clear-trigger, .x-form-field-wrap .x-form-search-trigger{ + cursor:pointer; +} + +.x-form-field-wrap .x-form-twin-triggers .x-form-trigger{ + position:static; + top:auto; + vertical-align:top; +} + +.x-form-field-wrap { + position:relative; + left:0;top:0; + text-align: left; + zoom:1; + white-space: nowrap; +} + +.ext-strict .ext-ie8 .x-toolbar-cell .x-form-field-trigger-wrap .x-form-trigger { + right: 0; /* IE8 Strict mode trigger bug */ +} + +.x-form-field-wrap .x-form-trigger-over{ + background-position:-17px 0; +} + +.x-form-field-wrap .x-form-trigger-click{ + background-position:-34px 0; +} + +.x-trigger-wrap-focus .x-form-trigger{ + background-position:-51px 0; +} + +.x-trigger-wrap-focus .x-form-trigger-over{ + background-position:-68px 0; +} + +.x-trigger-wrap-focus .x-form-trigger-click{ + background-position:-85px 0; +} + +.x-trigger-wrap-focus .x-form-trigger{ + border-bottom: 1px solid; +} + +.x-item-disabled .x-form-trigger-over{ + background-position:0 0 !important; + border-bottom: 1px solid; +} + +.x-item-disabled .x-form-trigger-click{ + background-position:0 0 !important; + border-bottom: 1px solid; +} + +.x-trigger-noedit{ + cursor:pointer; +} + +/* field focus style */ +.x-form-focus, textarea.x-form-focus{ + border: 1px solid; +} + +/* invalid fields */ +.x-form-invalid, textarea.x-form-invalid{ + background:repeat-x bottom; + border: 1px solid; +} + +.x-form-inner-invalid, textarea.x-form-inner-invalid{ + background:repeat-x bottom; +} + +/* editors */ +.x-editor { + visibility:hidden; + padding:0; + margin:0; +} + +.x-form-grow-sizer { + left: -10000px; + padding: 8px 3px; + position: absolute; + visibility:hidden; + top: -10000px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; + zoom:1; +} + +.x-form-grow-sizer p { + margin:0 !important; + border:0 none !important; + padding:0 !important; +} + +/* Form Items CSS */ + +.x-form-item { + display:block; + margin-bottom:4px; + zoom:1; +} + +.x-form-item label.x-form-item-label { + display:block; + float:left; + width:100px; + padding:3px; + padding-left:0; + clear:left; + z-index:2; + position:relative; +} + +.x-form-element { + padding-left:105px; + position:relative; +} + +.x-form-invalid-msg { + padding:2px; + padding-left:18px; + background: transparent no-repeat 0 2px; + line-height:16px; + width:200px; +} + +.x-form-label-left label.x-form-item-label { + text-align:left; +} + +.x-form-label-right label.x-form-item-label { + text-align:right; +} + +.x-form-label-top .x-form-item label.x-form-item-label { + width:auto; + float:none; + clear:none; + display:inline; + margin-bottom:4px; + position:static; +} + +.x-form-label-top .x-form-element { + padding-left:0; + padding-top:4px; +} + +.x-form-label-top .x-form-item { + padding-bottom:4px; +} + +/* Editor small font for grid, toolbar and tree */ +.x-small-editor .x-form-text { + height:20px; + line-height:16px; + vertical-align:middle; +} + +.ext-ie6 .x-small-editor .x-form-text, .ext-ie7 .x-small-editor .x-form-text { + margin-top:-1px !important; /* ie bogus margin bug */ + margin-bottom:-1px !important; + height:20px !important; /* ie quirks */ + line-height:16px !important; +} + +.ext-strict .x-small-editor .x-form-text { + height:16px !important; +} + +.ext-ie6 .x-small-editor .x-form-text, .ext-ie7 .x-small-editor .x-form-text { + height:20px; + line-height:16px; +} + +.ext-border-box .x-small-editor .x-form-text { + height:20px; +} + +.x-small-editor .x-form-select-one { + height:20px; + line-height:16px; + vertical-align:middle; +} + +.x-small-editor .x-form-num-field { + text-align:right; +} + +.x-small-editor .x-form-field-wrap .x-form-trigger{ + height:19px; +} + +.ext-webkit .x-small-editor .x-form-text{padding-top:3px;font-size:100%;} + +.x-form-clear { + clear:both; + height:0; + overflow:hidden; + line-height:0; + font-size:0; +} +.x-form-clear-left { + clear:left; + height:0; + overflow:hidden; + line-height:0; + font-size:0; +} + +.ext-ie6 .x-form-check-wrap input, .ext-border-box .x-form-check-wrap input{ + margin-top: 3px; +} + +.x-form-cb-label { + position: relative; + margin-left:4px; + top: 2px; +} + +.ext-ie .x-form-cb-label{ + top: 1px; +} + +.ext-ie6 .x-form-cb-label, .ext-border-box .x-form-cb-label{ + top: 3px; +} + +.x-form-display-field{ + padding-top: 2px; +} + +.ext-gecko .x-form-display-field, .ext-strict .ext-ie7 .x-form-display-field{ + padding-top: 1px; +} + +.ext-ie .x-form-display-field{ + padding-top: 3px; +} + +.ext-strict .ext-ie8 .x-form-display-field{ + padding-top: 0; +} + +.x-form-column { + float:left; + padding:0; + margin:0; + width:48%; + overflow:hidden; + zoom:1; +} + +/* buttons */ +.x-form .x-form-btns-ct .x-btn{ + float:right; + clear:none; +} + +.x-form .x-form-btns-ct .x-form-btns td { + border:0; + padding:0; +} + +.x-form .x-form-btns-ct .x-form-btns-right table{ + float:right; + clear:none; +} + +.x-form .x-form-btns-ct .x-form-btns-left table{ + float:left; + clear:none; +} + +.x-form .x-form-btns-ct .x-form-btns-center{ + text-align:center; /*ie*/ +} + +.x-form .x-form-btns-ct .x-form-btns-center table{ + margin:0 auto; /*everyone else*/ +} + +.x-form .x-form-btns-ct table td.x-form-btn-td{ + padding:3px; +} + +.x-form .x-form-btns-ct .x-btn-focus .x-btn-left{ + background-position:0 -147px; +} + +.x-form .x-form-btns-ct .x-btn-focus .x-btn-right{ + background-position:0 -168px; +} + +.x-form .x-form-btns-ct .x-btn-focus .x-btn-center{ + background-position:0 -189px; +} + +.x-form .x-form-btns-ct .x-btn-click .x-btn-center{ + background-position:0 -126px; +} + +.x-form .x-form-btns-ct .x-btn-click .x-btn-right{ + background-position:0 -84px; +} + +.x-form .x-form-btns-ct .x-btn-click .x-btn-left{ + background-position:0 -63px; +} + +.x-form-invalid-icon { + width:16px; + height:18px; + visibility:hidden; + position:absolute; + left:0; + top:0; + display:block; + background:transparent no-repeat 0 2px; +} + +/* fieldsets */ +.x-fieldset { + border:1px solid; + padding:10px; + margin-bottom:10px; + display:block; /* preserve margins in IE */ +} + +/* make top of checkbox/tools visible in webkit */ +.ext-webkit .x-fieldset-header { + padding-top: 1px; +} + +.ext-ie .x-fieldset legend { + margin-bottom:10px; +} + +.ext-ie .x-fieldset { + padding-top: 0; + padding-bottom:10px; +} + +.x-fieldset legend .x-tool-toggle { + margin-right:3px; + margin-left:0; + float:left !important; +} + +.x-fieldset legend input { + margin-right:3px; + float:left !important; + height:13px; + width:13px; +} + +fieldset.x-panel-collapsed { + padding-bottom:0 !important; + border-width: 1px 1px 0 1px !important; + border-left-color: transparent; + border-right-color: transparent; +} + +.ext-ie6 fieldset.x-panel-collapsed{ + padding-bottom:0 !important; + border-width: 1px 0 0 0 !important; + margin-left: 1px; + margin-right: 1px; +} + +fieldset.x-panel-collapsed .x-fieldset-bwrap { + visibility:hidden; + position:absolute; + left:-1000px; + top:-1000px; +} + +.ext-ie .x-fieldset-bwrap { + zoom:1; +} + +.x-fieldset-noborder { + border:0px none transparent; +} + +.x-fieldset-noborder legend { + margin-left:-3px; +} + +/* IE legend positioning bug */ +.ext-ie .x-fieldset-noborder legend { + position: relative; + margin-bottom:23px; +} +.ext-ie .x-fieldset-noborder legend span { + position: absolute; + left:16px; +} + +.ext-gecko .x-window-body .x-form-item { + -moz-outline: none; + outline: none; + overflow: auto; +} + +.ext-mac.ext-gecko .x-window-body .x-form-item { + overflow:hidden; +} + +.ext-gecko .x-form-item { + -moz-outline: none; + outline: none; +} + +.x-hide-label label.x-form-item-label { + display:none; +} + +.x-hide-label .x-form-element { + padding-left: 0 !important; +} + +.x-form-label-top .x-hide-label label.x-form-item-label{ + display: none; +} + +.x-fieldset { + overflow:hidden; +} + +.x-fieldset-bwrap { + overflow:hidden; + zoom:1; +} + +.x-fieldset-body { + overflow:hidden; +} +.x-btn{ + cursor:pointer; + white-space: nowrap; +} + +.x-btn button{ + border:0 none; + background-color:transparent; + padding-left:3px; + padding-right:3px; + cursor:pointer; + margin:0; + overflow:visible; + width:auto; + -moz-outline:0 none; + outline:0 none; +} + +* html .ext-ie .x-btn button { + width:1px; +} + +.ext-gecko .x-btn button, .ext-webkit .x-btn button { + padding-left:0; + padding-right:0; +} + +.ext-gecko .x-btn button::-moz-focus-inner { + padding:0; +} + +.ext-ie .x-btn button { + padding-top:2px; +} + +.x-btn td { + padding:0 !important; +} + +.x-btn-text { + cursor:pointer; + white-space: nowrap; + padding:0; +} + +/* icon placement and sizing styles */ + +/* Only text */ +.x-btn-noicon .x-btn-small .x-btn-text{ + height: 16px; +} + +.x-btn-noicon .x-btn-medium .x-btn-text{ + height: 24px; +} + +.x-btn-noicon .x-btn-large .x-btn-text{ + height: 32px; +} + +/* Only icons */ +.x-btn-icon .x-btn-text{ + background-position: center; + background-repeat: no-repeat; +} + +.x-btn-icon .x-btn-small .x-btn-text{ + height: 16px; + width: 16px; +} + +.x-btn-icon .x-btn-medium .x-btn-text{ + height: 24px; + width: 24px; +} + +.x-btn-icon .x-btn-large .x-btn-text{ + height: 32px; + width: 32px; +} + +/* Icons and text */ +/* left */ +.x-btn-text-icon .x-btn-icon-small-left .x-btn-text{ + background-position: 0 center; + background-repeat: no-repeat; + padding-left:18px; + height:16px; +} + +.x-btn-text-icon .x-btn-icon-medium-left .x-btn-text{ + background-position: 0 center; + background-repeat: no-repeat; + padding-left:26px; + height:24px; +} + +.x-btn-text-icon .x-btn-icon-large-left .x-btn-text{ + background-position: 0 center; + background-repeat: no-repeat; + padding-left:34px; + height:32px; +} + +/* top */ +.x-btn-text-icon .x-btn-icon-small-top .x-btn-text{ + background-position: center 0; + background-repeat: no-repeat; + padding-top:18px; +} + +.x-btn-text-icon .x-btn-icon-medium-top .x-btn-text{ + background-position: center 0; + background-repeat: no-repeat; + padding-top:26px; +} + +.x-btn-text-icon .x-btn-icon-large-top .x-btn-text{ + background-position: center 0; + background-repeat: no-repeat; + padding-top:34px; +} + +/* right */ +.x-btn-text-icon .x-btn-icon-small-right .x-btn-text{ + background-position: right center; + background-repeat: no-repeat; + padding-right:18px; + height:16px; +} + +.x-btn-text-icon .x-btn-icon-medium-right .x-btn-text{ + background-position: right center; + background-repeat: no-repeat; + padding-right:26px; + height:24px; +} + +.x-btn-text-icon .x-btn-icon-large-right .x-btn-text{ + background-position: right center; + background-repeat: no-repeat; + padding-right:34px; + height:32px; +} + +/* bottom */ +.x-btn-text-icon .x-btn-icon-small-bottom .x-btn-text{ + background-position: center bottom; + background-repeat: no-repeat; + padding-bottom:18px; +} + +.x-btn-text-icon .x-btn-icon-medium-bottom .x-btn-text{ + background-position: center bottom; + background-repeat: no-repeat; + padding-bottom:26px; +} + +.x-btn-text-icon .x-btn-icon-large-bottom .x-btn-text{ + background-position: center bottom; + background-repeat: no-repeat; + padding-bottom:34px; +} + +/* background positioning */ +.x-btn-tr i, .x-btn-tl i, .x-btn-mr i, .x-btn-ml i, .x-btn-br i, .x-btn-bl i{ + font-size:1px; + line-height:1px; + width:3px; + display:block; + overflow:hidden; +} + +.x-btn-tr i, .x-btn-tl i, .x-btn-br i, .x-btn-bl i{ + height:3px; +} + +.x-btn-tl{ + width:3px; + height:3px; + background:no-repeat 0 0; +} +.x-btn-tr{ + width:3px; + height:3px; + background:no-repeat -3px 0; +} +.x-btn-tc{ + height:3px; + background:repeat-x 0 -6px; +} + +.x-btn-ml{ + width:3px; + background:no-repeat 0 -24px; +} +.x-btn-mr{ + width:3px; + background:no-repeat -3px -24px; +} + +.x-btn-mc{ + background:repeat-x 0 -1096px; + vertical-align: middle; + text-align:center; + padding:0 5px; + cursor:pointer; + white-space:nowrap; +} + +/* Fixes an issue with the button height */ +.ext-strict .ext-ie6 .x-btn-mc, .ext-strict .ext-ie7 .x-btn-mc { + height: 100%; +} + +.x-btn-bl{ + width:3px; + height:3px; + background:no-repeat 0 -3px; +} + +.x-btn-br{ + width:3px; + height:3px; + background:no-repeat -3px -3px; +} + +.x-btn-bc{ + height:3px; + background:repeat-x 0 -15px; +} + +.x-btn-over .x-btn-tl{ + background-position: -6px 0; +} + +.x-btn-over .x-btn-tr{ + background-position: -9px 0; +} + +.x-btn-over .x-btn-tc{ + background-position: 0 -9px; +} + +.x-btn-over .x-btn-ml{ + background-position: -6px -24px; +} + +.x-btn-over .x-btn-mr{ + background-position: -9px -24px; +} + +.x-btn-over .x-btn-mc{ + background-position: 0 -2168px; +} + +.x-btn-over .x-btn-bl{ + background-position: -6px -3px; +} + +.x-btn-over .x-btn-br{ + background-position: -9px -3px; +} + +.x-btn-over .x-btn-bc{ + background-position: 0 -18px; +} + +.x-btn-click .x-btn-tl, .x-btn-menu-active .x-btn-tl, .x-btn-pressed .x-btn-tl{ + background-position: -12px 0; +} + +.x-btn-click .x-btn-tr, .x-btn-menu-active .x-btn-tr, .x-btn-pressed .x-btn-tr{ + background-position: -15px 0; +} + +.x-btn-click .x-btn-tc, .x-btn-menu-active .x-btn-tc, .x-btn-pressed .x-btn-tc{ + background-position: 0 -12px; +} + +.x-btn-click .x-btn-ml, .x-btn-menu-active .x-btn-ml, .x-btn-pressed .x-btn-ml{ + background-position: -12px -24px; +} + +.x-btn-click .x-btn-mr, .x-btn-menu-active .x-btn-mr, .x-btn-pressed .x-btn-mr{ + background-position: -15px -24px; +} + +.x-btn-click .x-btn-mc, .x-btn-menu-active .x-btn-mc, .x-btn-pressed .x-btn-mc{ + background-position: 0 -3240px; +} + +.x-btn-click .x-btn-bl, .x-btn-menu-active .x-btn-bl, .x-btn-pressed .x-btn-bl{ + background-position: -12px -3px; +} + +.x-btn-click .x-btn-br, .x-btn-menu-active .x-btn-br, .x-btn-pressed .x-btn-br{ + background-position: -15px -3px; +} + +.x-btn-click .x-btn-bc, .x-btn-menu-active .x-btn-bc, .x-btn-pressed .x-btn-bc{ + background-position: 0 -21px; +} + +.x-btn-disabled *{ + cursor:default !important; +} + + +/* With a menu arrow */ +/* right */ +.x-btn-mc em.x-btn-arrow { + display:block; + background:transparent no-repeat right center; + padding-right:10px; +} + +.x-btn-mc em.x-btn-split { + display:block; + background:transparent no-repeat right center; + padding-right:14px; +} + +/* bottom */ +.x-btn-mc em.x-btn-arrow-bottom { + display:block; + background:transparent no-repeat center bottom; + padding-bottom:14px; +} + +.x-btn-mc em.x-btn-split-bottom { + display:block; + background:transparent no-repeat center bottom; + padding-bottom:14px; +} + +/* height adjustment class */ +.x-btn-as-arrow .x-btn-mc em { + display:block; + background-color:transparent; + padding-bottom:14px; +} + +/* groups */ +.x-btn-group { + padding:1px; +} + +.x-btn-group-header { + padding:2px; + text-align:center; +} + +.x-btn-group-tc { + background: transparent repeat-x 0 0; + overflow:hidden; +} + +.x-btn-group-tl { + background: transparent no-repeat 0 0; + padding-left:3px; + zoom:1; +} + +.x-btn-group-tr { + background: transparent no-repeat right 0; + zoom:1; + padding-right:3px; +} + +.x-btn-group-bc { + background: transparent repeat-x 0 bottom; + zoom:1; +} + +.x-btn-group-bc .x-panel-footer { + zoom:1; +} + +.x-btn-group-bl { + background: transparent no-repeat 0 bottom; + padding-left:3px; + zoom:1; +} + +.x-btn-group-br { + background: transparent no-repeat right bottom; + padding-right:3px; + zoom:1; +} + +.x-btn-group-mc { + border:0 none; + padding:1px 0 0 0; + margin:0; +} + +.x-btn-group-mc .x-btn-group-body { + background-color:transparent; + border: 0 none; +} + +.x-btn-group-ml { + background: transparent repeat-y 0 0; + padding-left:3px; + zoom:1; +} + +.x-btn-group-mr { + background: transparent repeat-y right 0; + padding-right:3px; + zoom:1; +} + +.x-btn-group-bc .x-btn-group-footer { + padding-bottom:6px; +} + +.x-panel-nofooter .x-btn-group-bc { + height:3px; + font-size:0; + line-height:0; +} + +.x-btn-group-bwrap { + overflow:hidden; + zoom:1; +} + +.x-btn-group-body { + overflow:hidden; + zoom:1; +} + +.x-btn-group-notitle .x-btn-group-tc { + background: transparent repeat-x 0 0; + overflow:hidden; + height:2px; +}.x-toolbar{ + border-style:solid; + border-width:0 0 1px 0; + display: block; + padding:2px; + background:repeat-x top left; + position:relative; + left:0; + top:0; + zoom:1; + overflow:hidden; +} + +.x-toolbar-left { + width: 100%; +} + +.x-toolbar .x-item-disabled .x-btn-icon { + opacity: .35; + -moz-opacity: .35; + filter: alpha(opacity=35); +} + +.x-toolbar td { + vertical-align:middle; +} + +.x-toolbar td,.x-toolbar span,.x-toolbar input,.x-toolbar div,.x-toolbar select,.x-toolbar label{ + white-space: nowrap; +} + +.x-toolbar .x-item-disabled { + cursor:default; + opacity:.6; + -moz-opacity:.6; + filter:alpha(opacity=60); +} + +.x-toolbar .x-item-disabled * { + cursor:default; +} + +.x-toolbar .x-toolbar-cell { + vertical-align:middle; +} + +.x-toolbar .x-btn-tl, .x-toolbar .x-btn-tr, .x-toolbar .x-btn-tc, .x-toolbar .x-btn-ml, .x-toolbar .x-btn-mr, +.x-toolbar .x-btn-mc, .x-toolbar .x-btn-bl, .x-toolbar .x-btn-br, .x-toolbar .x-btn-bc +{ + background-position: 500px 500px; +} + +/* These rules are duplicated from button.css to give priority of x-toolbar rules above */ +.x-toolbar .x-btn-over .x-btn-tl{ + background-position: -6px 0; +} + +.x-toolbar .x-btn-over .x-btn-tr{ + background-position: -9px 0; +} + +.x-toolbar .x-btn-over .x-btn-tc{ + background-position: 0 -9px; +} + +.x-toolbar .x-btn-over .x-btn-ml{ + background-position: -6px -24px; +} + +.x-toolbar .x-btn-over .x-btn-mr{ + background-position: -9px -24px; +} + +.x-toolbar .x-btn-over .x-btn-mc{ + background-position: 0 -2168px; +} + +.x-toolbar .x-btn-over .x-btn-bl{ + background-position: -6px -3px; +} + +.x-toolbar .x-btn-over .x-btn-br{ + background-position: -9px -3px; +} + +.x-toolbar .x-btn-over .x-btn-bc{ + background-position: 0 -18px; +} + +.x-toolbar .x-btn-click .x-btn-tl, .x-toolbar .x-btn-menu-active .x-btn-tl, .x-toolbar .x-btn-pressed .x-btn-tl{ + background-position: -12px 0; +} + +.x-toolbar .x-btn-click .x-btn-tr, .x-toolbar .x-btn-menu-active .x-btn-tr, .x-toolbar .x-btn-pressed .x-btn-tr{ + background-position: -15px 0; +} + +.x-toolbar .x-btn-click .x-btn-tc, .x-toolbar .x-btn-menu-active .x-btn-tc, .x-toolbar .x-btn-pressed .x-btn-tc{ + background-position: 0 -12px; +} + +.x-toolbar .x-btn-click .x-btn-ml, .x-toolbar .x-btn-menu-active .x-btn-ml, .x-toolbar .x-btn-pressed .x-btn-ml{ + background-position: -12px -24px; +} + +.x-toolbar .x-btn-click .x-btn-mr, .x-toolbar .x-btn-menu-active .x-btn-mr, .x-toolbar .x-btn-pressed .x-btn-mr{ + background-position: -15px -24px; +} + +.x-toolbar .x-btn-click .x-btn-mc, .x-toolbar .x-btn-menu-active .x-btn-mc, .x-toolbar .x-btn-pressed .x-btn-mc{ + background-position: 0 -3240px; +} + +.x-toolbar .x-btn-click .x-btn-bl, .x-toolbar .x-btn-menu-active .x-btn-bl, .x-toolbar .x-btn-pressed .x-btn-bl{ + background-position: -12px -3px; +} + +.x-toolbar .x-btn-click .x-btn-br, .x-toolbar .x-btn-menu-active .x-btn-br, .x-toolbar .x-btn-pressed .x-btn-br{ + background-position: -15px -3px; +} + +.x-toolbar .x-btn-click .x-btn-bc, .x-toolbar .x-btn-menu-active .x-btn-bc, .x-toolbar .x-btn-pressed .x-btn-bc{ + background-position: 0 -21px; +} + +.x-toolbar div.xtb-text{ + padding:2px 2px 0; + line-height:16px; + display:block; +} + +.x-toolbar .xtb-sep { + background-position: center; + background-repeat: no-repeat; + display: block; + font-size: 1px; + height: 16px; + width:4px; + overflow: hidden; + cursor:default; + margin: 0 2px 0; + border:0; +} + +.x-toolbar .xtb-spacer { + width:2px; +} + +/* Paging Toolbar */ +.x-tbar-page-number{ + width:30px; + height:14px; +} + +.ext-ie .x-tbar-page-number{ + margin-top: 2px; +} + +.x-paging-info { + position:absolute; + top:5px; + right: 8px; +} + +/* floating */ +.x-toolbar-ct { + width:100%; +} + +.x-toolbar-right td { + text-align: center; +} + +.x-panel-tbar, .x-panel-bbar, .x-window-tbar, .x-window-bbar, .x-tab-panel-tbar, .x-tab-panel-bbar, .x-plain-tbar, .x-plain-bbar { + overflow:hidden; + zoom:1; +} + +.x-toolbar-more .x-btn-small .x-btn-text{ + height: 16px; + width: 12px; +} + +.x-toolbar-more em.x-btn-arrow { + display:inline; + background-color:transparent; + padding-right:0; +} + +.x-toolbar-more .x-btn-mc em.x-btn-arrow { + background-image: none; +} + +div.x-toolbar-no-items { + color:gray !important; + padding:5px 10px !important; +} + +/* fix ie toolbar form items */ +.ext-border-box .x-toolbar-cell .x-form-text { + margin-bottom:-1px !important; +} + +.ext-border-box .x-toolbar-cell .x-form-field-wrap .x-form-text { + margin:0 !important; +} + +.ext-ie .x-toolbar-cell .x-form-field-wrap { + height:21px; +} + +.ext-ie .x-toolbar-cell .x-form-text { + position:relative; + top:-1px; +} + +.ext-strict .ext-ie8 .x-toolbar-cell .x-form-field-trigger-wrap .x-form-text, .ext-strict .ext-ie .x-toolbar-cell .x-form-text { + top: 0px; +} + +.x-toolbar-right td .x-form-field-trigger-wrap{ + text-align: left; +} + +.x-toolbar-cell .x-form-checkbox, .x-toolbar-cell .x-form-radio{ + margin-top: 5px; +} + +.x-toolbar-cell .x-form-cb-label{ + vertical-align: bottom; + top: 1px; +} + +.ext-ie .x-toolbar-cell .x-form-checkbox, .ext-ie .x-toolbar-cell .x-form-radio{ + margin-top: 4px; +} + +.ext-ie .x-toolbar-cell .x-form-cb-label{ + top: 0; +} +/* Grid3 styles */ +.x-grid3 { + position:relative; + overflow:hidden; +} + +.x-grid-panel .x-panel-body { + overflow:hidden !important; +} + +.x-grid-panel .x-panel-mc .x-panel-body { + border:1px solid; +} + +.x-grid3 table { + table-layout:fixed; +} + +.x-grid3-viewport{ + overflow:hidden; +} + +.x-grid3-hd-row td, .x-grid3-row td, .x-grid3-summary-row td{ + -moz-outline: none; + outline: none; + -moz-user-focus: normal; +} + +.x-grid3-row td, .x-grid3-summary-row td { + line-height:13px; + vertical-align: top; + padding-left:1px; + padding-right:1px; + -moz-user-select: none; + -khtml-user-select:none; + -webkit-user-select:ignore; +} + +.x-grid3-cell{ + -moz-user-select: none; + -khtml-user-select:none; + -webkit-user-select:ignore; +} + +.x-grid3-hd-row td { + line-height:15px; + vertical-align:middle; + border-left:1px solid; + border-right:1px solid; +} + +.x-grid3-hd-row .x-grid3-marker-hd { + padding:3px; +} + +.x-grid3-row .x-grid3-marker { + padding:3px; +} + +.x-grid3-cell-inner, .x-grid3-hd-inner{ + overflow:hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + padding:3px 3px 3px 5px; + white-space: nowrap; +} + +/* ActionColumn, reduce padding to accommodate 16x16 icons in normal row height */ +.x-action-col-cell .x-grid3-cell-inner { + padding-top: 1px; + padding-bottom: 1px; +} + +.x-action-col-icon { + cursor: pointer; +} + +.x-grid3-hd-inner { + position:relative; + cursor:inherit; + padding:4px 3px 4px 5px; +} + +.x-grid3-row-body { + white-space:normal; +} + +.x-grid3-body-cell { + -moz-outline:0 none; + outline:0 none; +} + +/* IE Quirks to clip */ +.ext-ie .x-grid3-cell-inner, .ext-ie .x-grid3-hd-inner{ + width:100%; +} + +/* reverse above in strict mode */ +.ext-strict .x-grid3-cell-inner, .ext-strict .x-grid3-hd-inner{ + width:auto; +} + +.x-grid-row-loading { + background: no-repeat center center; +} + +.x-grid-page { + overflow:hidden; +} + +.x-grid3-row { + cursor: default; + border: 1px solid; + width:100%; +} + +.x-grid3-row-over { + border:1px solid; + background: repeat-x left top; +} + +.x-grid3-resize-proxy { + width:1px; + left:0; + cursor: e-resize; + cursor: col-resize; + position:absolute; + top:0; + height:100px; + overflow:hidden; + visibility:hidden; + border:0 none; + z-index:7; +} + +.x-grid3-resize-marker { + width:1px; + left:0; + position:absolute; + top:0; + height:100px; + overflow:hidden; + visibility:hidden; + border:0 none; + z-index:7; +} + +.x-grid3-focus { + position:absolute; + left:0; + top:0; + width:1px; + height:1px; + line-height:1px; + font-size:1px; + -moz-outline:0 none; + outline:0 none; + -moz-user-select: text; + -khtml-user-select: text; + -webkit-user-select:ignore; +} + +/* header styles */ +.x-grid3-header{ + background: repeat-x 0 bottom; + cursor:default; + zoom:1; + padding:1px 0 0 0; +} + +.x-grid3-header-pop { + border-left:1px solid; + float:right; + clear:none; +} + +.x-grid3-header-pop-inner { + border-left:1px solid; + width:14px; + height:19px; + background: transparent no-repeat center center; +} + +.ext-ie .x-grid3-header-pop-inner { + width:15px; +} + +.ext-strict .x-grid3-header-pop-inner { + width:14px; +} + +.x-grid3-header-inner { + overflow:hidden; + zoom:1; + float:left; +} + +.x-grid3-header-offset { + padding-left:1px; + text-align: left; +} + +td.x-grid3-hd-over, td.sort-desc, td.sort-asc, td.x-grid3-hd-menu-open { + border-left:1px solid; + border-right:1px solid; +} + +td.x-grid3-hd-over .x-grid3-hd-inner, td.sort-desc .x-grid3-hd-inner, td.sort-asc .x-grid3-hd-inner, td.x-grid3-hd-menu-open .x-grid3-hd-inner { + background: repeat-x left bottom; + +} + +.x-grid3-sort-icon{ + background-repeat: no-repeat; + display: none; + height: 4px; + width: 13px; + margin-left:3px; + vertical-align: middle; +} + +.sort-asc .x-grid3-sort-icon, .sort-desc .x-grid3-sort-icon { + display: inline; +} + +/* Header position fixes for IE strict mode */ +.ext-strict .ext-ie .x-grid3-header-inner, .ext-strict .ext-ie6 .x-grid3-hd { + position:relative; +} + +.ext-strict .ext-ie6 .x-grid3-hd-inner{ + position:static; +} + +/* Body Styles */ +.x-grid3-body { + zoom:1; +} + +.x-grid3-scroller { + overflow:auto; + zoom:1; + position:relative; +} + +.x-grid3-cell-text, .x-grid3-hd-text { + display: block; + padding: 3px 5px 3px 5px; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select:ignore; +} + +.x-grid3-split { + background-position: center; + background-repeat: no-repeat; + cursor: e-resize; + cursor: col-resize; + display: block; + font-size: 1px; + height: 16px; + overflow: hidden; + position: absolute; + top: 2px; + width: 6px; + z-index: 3; +} + +/* Column Reorder DD */ +.x-dd-drag-proxy .x-grid3-hd-inner{ + background: repeat-x left bottom; + width:120px; + padding:3px; + border:1px solid; + overflow:hidden; +} + +.col-move-top, .col-move-bottom{ + width:9px; + height:9px; + position:absolute; + top:0; + line-height:1px; + font-size:1px; + overflow:hidden; + visibility:hidden; + z-index:20000; + background:transparent no-repeat left top; +} + +/* Selection Styles */ +.x-grid3-row-selected { + border:1px dotted; +} + +.x-grid3-locked td.x-grid3-row-marker, .x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker{ + background: repeat-x 0 bottom !important; + vertical-align:middle !important; + padding:0; + border-top:1px solid; + border-bottom:none !important; + border-right:1px solid !important; + text-align:center; +} + +.x-grid3-locked td.x-grid3-row-marker div, .x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker div{ + padding:0 4px; + text-align:center; +} + +/* dirty cells */ +.x-grid3-dirty-cell { + background: transparent no-repeat 0 0; +} + +/* Grid Toolbars */ +.x-grid3-topbar, .x-grid3-bottombar{ + overflow:hidden; + display:none; + zoom:1; + position:relative; +} + +.x-grid3-topbar .x-toolbar{ + border-right:0 none; +} + +.x-grid3-bottombar .x-toolbar{ + border-right:0 none; + border-bottom:0 none; + border-top:1px solid; +} + +/* Props Grid Styles */ +.x-props-grid .x-grid3-cell{ + padding:1px; +} + +.x-props-grid .x-grid3-td-name .x-grid3-cell-inner{ + background:transparent repeat-y -16px !important; + padding-left:12px; +} + +.x-props-grid .x-grid3-body .x-grid3-td-name{ + padding:1px; + padding-right:0; + border:0 none; + border-right:1px solid; +} + +/* dd */ +.x-grid3-col-dd { + border:0 none; + padding:0; + background-color:transparent; +} + +.x-dd-drag-ghost .x-grid3-dd-wrap { + padding:1px 3px 3px 1px; +} + +.x-grid3-hd { + -moz-user-select:none; + -khtml-user-select:none; + -webkit-user-select:ignore; +} + +.x-grid3-hd-btn { + display:none; + position:absolute; + width:14px; + background:no-repeat left center; + right:0; + top:0; + z-index:2; + cursor:pointer; +} + +.x-grid3-hd-over .x-grid3-hd-btn, .x-grid3-hd-menu-open .x-grid3-hd-btn { + display:block; +} + +a.x-grid3-hd-btn:hover { + background-position:-14px center; +} + +/* Expanders */ +.x-grid3-body .x-grid3-td-expander { + background:transparent repeat-y right; +} + +.x-grid3-body .x-grid3-td-expander .x-grid3-cell-inner { + padding:0 !important; + height:100%; +} + +.x-grid3-row-expander { + width:100%; + height:18px; + background-position:4px 2px; + background-repeat:no-repeat; + background-color:transparent; +} + +.x-grid3-row-collapsed .x-grid3-row-expander { + background-position:4px 2px; +} + +.x-grid3-row-expanded .x-grid3-row-expander { + background-position:-21px 2px; +} + +.x-grid3-row-collapsed .x-grid3-row-body { + display:none !important; +} + +.x-grid3-row-expanded .x-grid3-row-body { + display:block !important; +} + +/* Checkers */ +.x-grid3-body .x-grid3-td-checker { + background:transparent repeat-y right; +} + +.x-grid3-body .x-grid3-td-checker .x-grid3-cell-inner, .x-grid3-header .x-grid3-td-checker .x-grid3-hd-inner { + padding:0 !important; + height:100%; +} + +.x-grid3-row-checker, .x-grid3-hd-checker { + width:100%; + height:18px; + background-position:2px 2px; + background-repeat:no-repeat; + background-color:transparent; +} + +.x-grid3-row .x-grid3-row-checker { + background-position:2px 2px; +} + +.x-grid3-row-selected .x-grid3-row-checker, .x-grid3-hd-checker-on .x-grid3-hd-checker,.x-grid3-row-checked .x-grid3-row-checker { + background-position:-23px 2px; +} + +.x-grid3-hd-checker { + background-position:2px 1px; +} + +.ext-border-box .x-grid3-hd-checker { + background-position:2px 3px; +} + +.x-grid3-hd-checker-on .x-grid3-hd-checker { + background-position:-23px 1px; +} + +.ext-border-box .x-grid3-hd-checker-on .x-grid3-hd-checker { + background-position:-23px 3px; +} + +/* Numberer */ +.x-grid3-body .x-grid3-td-numberer { + background:transparent repeat-y right; +} + +.x-grid3-body .x-grid3-td-numberer .x-grid3-cell-inner { + padding:3px 5px 0 0 !important; + text-align:right; +} + +/* Row Icon */ + +.x-grid3-body .x-grid3-td-row-icon { + background:transparent repeat-y right; + vertical-align:top; + text-align:center; +} + +.x-grid3-body .x-grid3-td-row-icon .x-grid3-cell-inner { + padding:0 !important; + background-position:center center; + background-repeat:no-repeat; + width:16px; + height:16px; + margin-left:2px; + margin-top:3px; +} + +/* All specials */ +.x-grid3-body .x-grid3-row-selected .x-grid3-td-numberer, +.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker, +.x-grid3-body .x-grid3-row-selected .x-grid3-td-expander { + background:transparent repeat-y right; +} + +.x-grid3-body .x-grid3-check-col-td .x-grid3-cell-inner { + padding: 1px 0 0 0 !important; +} + +.x-grid3-check-col { + width:100%; + height:16px; + background-position:center center; + background-repeat:no-repeat; + background-color:transparent; +} + +.x-grid3-check-col-on { + width:100%; + height:16px; + background-position:center center; + background-repeat:no-repeat; + background-color:transparent; +} + +/* Grouping classes */ +.x-grid-group, .x-grid-group-body, .x-grid-group-hd { + zoom:1; +} + +.x-grid-group-hd { + border-bottom: 2px solid; + cursor:pointer; + padding-top:6px; +} + +.x-grid-group-hd div.x-grid-group-title { + background:transparent no-repeat 3px 3px; + padding:4px 4px 4px 17px; +} + +.x-grid-group-collapsed .x-grid-group-body { + display:none; +} + +.ext-ie6 .x-grid3 .x-editor .x-form-text, .ext-ie7 .x-grid3 .x-editor .x-form-text { + position:relative; + top:-1px; +} + +.ext-ie .x-props-grid .x-editor .x-form-text { + position:static; + top:0; +} + +.x-grid-empty { + padding:10px; +} + +/* fix floating toolbar issue */ +.ext-ie7 .x-grid-panel .x-panel-bbar { + position:relative; +} + + +/* Reset position to static when Grid Panel has been framed */ +/* to resolve 'snapping' from top to bottom behavior. */ +/* @forumThread 86656 */ +.ext-ie7 .x-grid-panel .x-panel-mc .x-panel-bbar { + position: static; +} + +.ext-ie6 .x-grid3-header { + position: relative; +} + +/* Fix WebKit bug in Grids */ +.ext-webkit .x-grid-panel .x-panel-bwrap{ + -webkit-user-select:none; +} +.ext-webkit .x-tbar-page-number{ + -webkit-user-select:ignore; +} +/* end*/ + +/* column lines */ +.x-grid-with-col-lines .x-grid3-row td.x-grid3-cell { + padding-right:0; + border-right:1px solid; +} +.x-pivotgrid .x-grid3-header-offset table { + width: 100%; + border-collapse: collapse; +} + +.x-pivotgrid .x-grid3-header-offset table td { + padding: 4px 3px 4px 5px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 11px; + line-height: 13px; + font-family: tahoma; +} + +.x-pivotgrid .x-grid3-row-headers { + display: block; + float: left; +} + +.x-pivotgrid .x-grid3-row-headers table { + height: 100%; + width: 100%; + border-collapse: collapse; +} + +.x-pivotgrid .x-grid3-row-headers table td { + height: 18px; + padding: 2px 7px 0 0; + text-align: right; + text-overflow: ellipsis; + font-size: 11px; + font-family: tahoma; +} + +.ext-gecko .x-pivotgrid .x-grid3-row-headers table td { + height: 21px; +} + +.x-grid3-header-title { + top: 0%; + left: 0%; + position: absolute; + text-align: center; + vertical-align: middle; + font-family: tahoma; + font-size: 11px; + padding: auto 1px; + display: table-cell; +} + +.x-grid3-header-title span { + position: absolute; + top: 50%; + left: 0%; + width: 100%; + margin-top: -6px; +}.x-dd-drag-proxy{ + position:absolute; + left:0; + top:0; + visibility:hidden; + z-index:15000; +} + +.x-dd-drag-ghost{ + -moz-opacity: 0.85; + opacity:.85; + filter: alpha(opacity=85); + border: 1px solid; + padding:3px; + padding-left:20px; + white-space:nowrap; +} + +.x-dd-drag-repair .x-dd-drag-ghost{ + -moz-opacity: 0.4; + opacity:.4; + filter: alpha(opacity=40); + border:0 none; + padding:0; + background-color:transparent; +} + +.x-dd-drag-repair .x-dd-drop-icon{ + visibility:hidden; +} + +.x-dd-drop-icon{ + position:absolute; + top:3px; + left:3px; + display:block; + width:16px; + height:16px; + background-color:transparent; + background-position: center; + background-repeat: no-repeat; + z-index:1; +} + +.x-view-selector { + position:absolute; + left:0; + top:0; + width:0; + border:1px dotted; + opacity: .5; + -moz-opacity: .5; + filter:alpha(opacity=50); + zoom:1; +}.ext-strict .ext-ie .x-tree .x-panel-bwrap{ + position:relative; + overflow:hidden; +} + +.x-tree-icon, .x-tree-ec-icon, .x-tree-elbow-line, .x-tree-elbow, .x-tree-elbow-end, .x-tree-elbow-plus, .x-tree-elbow-minus, .x-tree-elbow-end-plus, .x-tree-elbow-end-minus{ + border: 0 none; + height: 18px; + margin: 0; + padding: 0; + vertical-align: top; + width: 16px; + background-repeat: no-repeat; +} + +.x-tree-node-collapsed .x-tree-node-icon, .x-tree-node-expanded .x-tree-node-icon, .x-tree-node-leaf .x-tree-node-icon{ + border: 0 none; + height: 18px; + margin: 0; + padding: 0; + vertical-align: top; + width: 16px; + background-position:center; + background-repeat: no-repeat; +} + +.ext-ie .x-tree-node-indent img, .ext-ie .x-tree-node-icon, .ext-ie .x-tree-ec-icon { + vertical-align: middle !important; +} + +.ext-strict .ext-ie8 .x-tree-node-indent img, .ext-strict .ext-ie8 .x-tree-node-icon, .ext-strict .ext-ie8 .x-tree-ec-icon { + vertical-align: top !important; +} + +/* checkboxes */ + +input.x-tree-node-cb { + margin-left:1px; + height: 19px; + vertical-align: bottom; +} + +.ext-ie input.x-tree-node-cb { + margin-left:0; + margin-top: 1px; + width: 16px; + height: 16px; + vertical-align: middle; +} + +.ext-strict .ext-ie8 input.x-tree-node-cb{ + margin: 1px 1px; + height: 14px; + vertical-align: bottom; +} + +.ext-strict .ext-ie8 input.x-tree-node-cb + a{ + vertical-align: bottom; +} + +.ext-opera input.x-tree-node-cb { + height: 14px; + vertical-align: middle; +} + +.x-tree-noicon .x-tree-node-icon{ + width:0; height:0; +} + +/* No line styles */ +.x-tree-no-lines .x-tree-elbow{ + background-color:transparent; +} + +.x-tree-no-lines .x-tree-elbow-end{ + background-color:transparent; +} + +.x-tree-no-lines .x-tree-elbow-line{ + background-color:transparent; +} + +/* Arrows */ +.x-tree-arrows .x-tree-elbow{ + background-color:transparent; +} + +.x-tree-arrows .x-tree-elbow-plus{ + background:transparent no-repeat 0 0; +} + +.x-tree-arrows .x-tree-elbow-minus{ + background:transparent no-repeat -16px 0; +} + +.x-tree-arrows .x-tree-elbow-end{ + background-color:transparent; +} + +.x-tree-arrows .x-tree-elbow-end-plus{ + background:transparent no-repeat 0 0; +} + +.x-tree-arrows .x-tree-elbow-end-minus{ + background:transparent no-repeat -16px 0; +} + +.x-tree-arrows .x-tree-elbow-line{ + background-color:transparent; +} + +.x-tree-arrows .x-tree-ec-over .x-tree-elbow-plus{ + background-position:-32px 0; +} + +.x-tree-arrows .x-tree-ec-over .x-tree-elbow-minus{ + background-position:-48px 0; +} + +.x-tree-arrows .x-tree-ec-over .x-tree-elbow-end-plus{ + background-position:-32px 0; +} + +.x-tree-arrows .x-tree-ec-over .x-tree-elbow-end-minus{ + background-position:-48px 0; +} + +.x-tree-elbow-plus, .x-tree-elbow-minus, .x-tree-elbow-end-plus, .x-tree-elbow-end-minus{ + cursor:pointer; +} + +.ext-ie ul.x-tree-node-ct{ + font-size:0; + line-height:0; + zoom:1; +} + +.x-tree-node{ + white-space: nowrap; +} + +.x-tree-node-el { + line-height:18px; + cursor:pointer; +} + +.x-tree-node a, .x-dd-drag-ghost a{ + text-decoration:none; + -khtml-user-select:none; + -moz-user-select:none; + -webkit-user-select:ignore; + -kthml-user-focus:normal; + -moz-user-focus:normal; + -moz-outline: 0 none; + outline:0 none; +} + +.x-tree-node a span, .x-dd-drag-ghost a span{ + text-decoration:none; + padding:1px 3px 1px 2px; +} + +.x-tree-node .x-tree-node-disabled .x-tree-node-icon{ + -moz-opacity: 0.5; + opacity:.5; + filter: alpha(opacity=50); +} + +.x-tree-node .x-tree-node-inline-icon{ + background-color:transparent; +} + +.x-tree-node a:hover, .x-dd-drag-ghost a:hover{ + text-decoration:none; +} + +.x-tree-node div.x-tree-drag-insert-below{ + border-bottom:1px dotted; +} + +.x-tree-node div.x-tree-drag-insert-above{ + border-top:1px dotted; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below{ + border-bottom:0 none; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above{ + border-top:0 none; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a{ + border-bottom:2px solid; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a{ + border-top:2px solid; +} + +.x-tree-node .x-tree-drag-append a span{ + border:1px dotted; +} + +.x-dd-drag-ghost .x-tree-node-indent, .x-dd-drag-ghost .x-tree-ec-icon{ + display:none !important; +} + +/* Fix for ie rootVisible:false issue */ +.x-tree-root-ct { + zoom:1; +} +.x-date-picker { + border: 1px solid; + border-top:0 none; + position:relative; +} + +.x-date-picker a { + -moz-outline:0 none; + outline:0 none; +} + +.x-date-inner, .x-date-inner td, .x-date-inner th{ + border-collapse:separate; +} + +.x-date-middle,.x-date-left,.x-date-right { + background: repeat-x 0 -83px; + overflow:hidden; +} + +.x-date-middle .x-btn-tc,.x-date-middle .x-btn-tl,.x-date-middle .x-btn-tr, +.x-date-middle .x-btn-mc,.x-date-middle .x-btn-ml,.x-date-middle .x-btn-mr, +.x-date-middle .x-btn-bc,.x-date-middle .x-btn-bl,.x-date-middle .x-btn-br{ + background:transparent !important; + vertical-align:middle; +} + +.x-date-middle .x-btn-mc em.x-btn-arrow { + background:transparent no-repeat right 0; +} + +.x-date-right, .x-date-left { + width:18px; +} + +.x-date-right{ + text-align:right; +} + +.x-date-middle { + padding-top:2px; + padding-bottom:2px; + width:130px; /* FF3 */ +} + +.x-date-right a, .x-date-left a{ + display:block; + width:16px; + height:16px; + background-position: center; + background-repeat: no-repeat; + cursor:pointer; + -moz-opacity: 0.6; + opacity:.6; + filter: alpha(opacity=60); +} + +.x-date-right a:hover, .x-date-left a:hover{ + -moz-opacity: 1; + opacity:1; + filter: alpha(opacity=100); +} + +.x-item-disabled .x-date-right a:hover, .x-item-disabled .x-date-left a:hover{ + -moz-opacity: 0.6; + opacity:.6; + filter: alpha(opacity=60); +} + +.x-date-right a { + margin-right:2px; + text-decoration:none !important; +} + +.x-date-left a{ + margin-left:2px; + text-decoration:none !important; +} + +table.x-date-inner { + width: 100%; + table-layout:fixed; +} + +.ext-webkit table.x-date-inner{ + /* Fix for webkit browsers */ + width: 175px; +} + + +.x-date-inner th { + width:25px; +} + +.x-date-inner th { + background: repeat-x left top; + text-align:right !important; + border-bottom: 1px solid; + cursor:default; + padding:0; + border-collapse:separate; +} + +.x-date-inner th span { + display:block; + padding:2px; + padding-right:7px; +} + +.x-date-inner td { + border: 1px solid; + text-align:right; + padding:0; +} + +.x-date-inner a { + padding:2px 5px; + display:block; + text-decoration:none; + text-align:right; + zoom:1; +} + +.x-date-inner .x-date-active{ + cursor:pointer; + color:black; +} + +.x-date-inner .x-date-selected a{ + background: repeat-x left top; + border:1px solid; + padding:1px 4px; +} + +.x-date-inner .x-date-today a{ + border: 1px solid; + padding:1px 4px; +} + +.x-date-inner .x-date-prevday a,.x-date-inner .x-date-nextday a { + text-decoration:none !important; +} + +.x-date-bottom { + padding:4px; + border-top: 1px solid; + background: repeat-x left top; +} + +.x-date-inner a:hover, .x-date-inner .x-date-disabled a:hover{ + text-decoration:none !important; +} + +.x-item-disabled .x-date-inner a:hover{ + background: none; +} + +.x-date-inner .x-date-disabled a { + cursor:default; +} + +.x-date-menu .x-menu-item { + padding:1px 24px 1px 4px; + white-space: nowrap; +} + +.x-date-menu .x-menu-item .x-menu-item-icon { + width:10px; + height:10px; + margin-right:5px; + background-position:center -4px !important; +} + +.x-date-mp { + position:absolute; + left:0; + top:0; + display:none; +} + +.x-date-mp td { + padding:2px; + font:normal 11px arial, helvetica,tahoma,sans-serif; +} + +td.x-date-mp-month,td.x-date-mp-year,td.x-date-mp-ybtn { + border: 0 none; + text-align:center; + vertical-align: middle; + width:25%; +} + +.x-date-mp-ok { + margin-right:3px; +} + +.x-date-mp-btns button { + text-decoration:none; + text-align:center; + text-decoration:none !important; + border:1px solid; + padding:1px 3px 1px; + cursor:pointer; +} + +.x-date-mp-btns { + background: repeat-x left top; +} + +.x-date-mp-btns td { + border-top: 1px solid; + text-align:center; +} + +td.x-date-mp-month a,td.x-date-mp-year a { + display:block; + padding:2px 4px; + text-decoration:none; + text-align:center; +} + +td.x-date-mp-month a:hover,td.x-date-mp-year a:hover { + text-decoration:none; + cursor:pointer; +} + +td.x-date-mp-sel a { + padding:1px 3px; + background: repeat-x left top; + border:1px solid; +} + +.x-date-mp-ybtn a { + overflow:hidden; + width:15px; + height:15px; + cursor:pointer; + background:transparent no-repeat; + display:block; + margin:0 auto; +} + +.x-date-mp-ybtn a.x-date-mp-next { + background-position:0 -120px; +} + +.x-date-mp-ybtn a.x-date-mp-next:hover { + background-position:-15px -120px; +} + +.x-date-mp-ybtn a.x-date-mp-prev { + background-position:0 -105px; +} + +.x-date-mp-ybtn a.x-date-mp-prev:hover { + background-position:-15px -105px; +} + +.x-date-mp-ybtn { + text-align:center; +} + +td.x-date-mp-sep { + border-right:1px solid; +}.x-tip{ + position: absolute; + top: 0; + left:0; + visibility: hidden; + z-index: 20002; + border:0 none; +} + +.x-tip .x-tip-close{ + height: 15px; + float:right; + width: 15px; + margin:0 0 2px 2px; + cursor:pointer; + display:none; +} + +.x-tip .x-tip-tc { + background: transparent no-repeat 0 -62px; + padding-top:3px; + overflow:hidden; + zoom:1; +} + +.x-tip .x-tip-tl { + background: transparent no-repeat 0 0; + padding-left:6px; + overflow:hidden; + zoom:1; +} + +.x-tip .x-tip-tr { + background: transparent no-repeat right 0; + padding-right:6px; + overflow:hidden; + zoom:1; +} + +.x-tip .x-tip-bc { + background: transparent no-repeat 0 -121px; + height:3px; + overflow:hidden; +} + +.x-tip .x-tip-bl { + background: transparent no-repeat 0 -59px; + padding-left:6px; + zoom:1; +} + +.x-tip .x-tip-br { + background: transparent no-repeat right -59px; + padding-right:6px; + zoom:1; +} + +.x-tip .x-tip-mc { + border:0 none; +} + +.x-tip .x-tip-ml { + background: no-repeat 0 -124px; + padding-left:6px; + zoom:1; +} + +.x-tip .x-tip-mr { + background: transparent no-repeat right -124px; + padding-right:6px; + zoom:1; +} + +.ext-ie .x-tip .x-tip-header,.ext-ie .x-tip .x-tip-tc { + font-size:0; + line-height:0; +} + +.ext-border-box .x-tip .x-tip-header, .ext-border-box .x-tip .x-tip-tc{ + line-height: 1px; +} + +.x-tip .x-tip-header-text { + padding:0; + margin:0 0 2px 0; +} + +.x-tip .x-tip-body { + margin:0 !important; + line-height:14px; + padding:0; +} + +.x-tip .x-tip-body .loading-indicator { + margin:0; +} + +.x-tip-draggable .x-tip-header,.x-tip-draggable .x-tip-header-text { + cursor:move; +} + +.x-form-invalid-tip .x-tip-tc { + background: repeat-x 0 -12px; + padding-top:6px; +} + +.x-form-invalid-tip .x-tip-bc { + background: repeat-x 0 -18px; + height:6px; +} + +.x-form-invalid-tip .x-tip-bl { + background: no-repeat 0 -6px; +} + +.x-form-invalid-tip .x-tip-br { + background: no-repeat right -6px; +} + +.x-form-invalid-tip .x-tip-body { + padding:2px; +} + +.x-form-invalid-tip .x-tip-body { + padding-left:24px; + background:transparent no-repeat 2px 2px; +} + +.x-tip-anchor { + position: absolute; + width: 9px; + height: 10px; + overflow:hidden; + background: transparent no-repeat 0 0; + zoom:1; +} +.x-tip-anchor-bottom { + background-position: -9px 0; +} +.x-tip-anchor-right { + background-position: -18px 0; + width: 10px; +} +.x-tip-anchor-left { + background-position: -28px 0; + width: 10px; +}.x-menu { + z-index: 15000; + zoom: 1; + background: repeat-y; +} + +.x-menu-floating{ + border: 1px solid; +} + +.x-menu a { + text-decoration: none !important; +} + +.ext-ie .x-menu { + zoom:1; + overflow:hidden; +} + +.x-menu-list{ + padding: 2px; + background-color:transparent; + border:0 none; + overflow:hidden; + overflow-y: hidden; +} + +.ext-strict .ext-ie .x-menu-list{ + position: relative; +} + +.x-menu li{ + line-height:100%; +} + +.x-menu li.x-menu-sep-li{ + font-size:1px; + line-height:1px; +} + +.x-menu-list-item{ + white-space: nowrap; + display:block; + padding:1px; +} + +.x-menu-item{ + -moz-user-select: none; + -khtml-user-select:none; + -webkit-user-select:ignore; +} + +.x-menu-item-arrow{ + background:transparent no-repeat right; +} + +.x-menu-sep { + display:block; + font-size:1px; + line-height:1px; + margin: 2px 3px; + border-bottom:1px solid; + overflow:hidden; +} + +.x-menu-focus { + position:absolute; + left:-1px; + top:-1px; + width:1px; + height:1px; + line-height:1px; + font-size:1px; + -moz-outline:0 none; + outline:0 none; + -moz-user-select: none; + -khtml-user-select:none; + -webkit-user-select:ignore; + overflow:hidden; + display:block; +} + +a.x-menu-item { + cursor: pointer; + display: block; + line-height: 16px; + outline-color: -moz-use-text-color; + outline-style: none; + outline-width: 0; + padding: 3px 21px 3px 27px; + position: relative; + text-decoration: none; + white-space: nowrap; +} + +.x-menu-item-active { + background-repeat: repeat-x; + background-position: left bottom; + border-style:solid; + border-width: 1px 0; + margin:0 1px; + padding: 0; +} + +.x-menu-item-active a.x-menu-item { + border-style:solid; + border-width:0 1px; + margin:0 -1px; +} + +.x-menu-item-icon { + border: 0 none; + height: 16px; + padding: 0; + vertical-align: top; + width: 16px; + position: absolute; + left: 3px; + top: 3px; + margin: 0; + background-position:center; +} + +.ext-ie .x-menu-item-icon { + left: -24px; +} +.ext-strict .x-menu-item-icon { + left: 3px; +} + +.ext-ie6 .x-menu-item-icon { + left: -24px; +} + +.ext-ie .x-menu-item-icon { + vertical-align: middle; +} + +.x-menu-check-item .x-menu-item-icon{ + background: transparent no-repeat center; +} + +.x-menu-group-item .x-menu-item-icon{ + background-color: transparent; +} + +.x-menu-item-checked .x-menu-group-item .x-menu-item-icon{ + background: transparent no-repeat center; +} + +.x-date-menu .x-menu-list{ + padding: 0; +} + +.x-menu-date-item{ + padding:0; +} + +.x-menu .x-color-palette, .x-menu .x-date-picker{ + margin-left: 26px; + margin-right:4px; +} + +.x-menu .x-date-picker{ + border:1px solid; + margin-top:2px; + margin-bottom:2px; +} + +.x-menu-plain .x-color-palette, .x-menu-plain .x-date-picker{ + margin: 0; + border: 0 none; +} + +.x-date-menu { + padding:0 !important; +} + +/* + * fixes separator visibility problem in IE 6 + */ +.ext-strict .ext-ie6 .x-menu-sep-li { + padding: 3px 4px; +} +.ext-strict .ext-ie6 .x-menu-sep { + margin: 0; + height: 1px; +} + +/* + * Fixes an issue with "fat" separators in webkit + */ +.ext-webkit .x-menu-sep{ + height: 1px; +} + +/* + * Ugly mess to remove the white border under the picker + */ +.ext-ie .x-date-menu{ + height: 199px; +} + +.ext-strict .ext-ie .x-date-menu, .ext-border-box .ext-ie8 .x-date-menu{ + height: 197px; +} + +.ext-strict .ext-ie7 .x-date-menu{ + height: 195px; +} + +.ext-strict .ext-ie8 .x-date-menu{ + height: auto; +} + +.x-cycle-menu .x-menu-item-checked { + border:1px dotted !important; + padding:0; +} + +.x-menu .x-menu-scroller { + width: 100%; + background-repeat:no-repeat; + background-position:center; + height:8px; + line-height: 8px; + cursor:pointer; + margin: 0; + padding: 0; +} + +.x-menu .x-menu-scroller-active{ + height: 6px; + line-height: 6px; +} + +.x-menu-list-item-indent{ + padding-left: 27px; +}/* + Creates rounded, raised boxes like on the Ext website - the markup isn't pretty: + <div class="x-box-blue"> + <div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div> + <div class="x-box-ml"><div class="x-box-mr"><div class="x-box-mc"> + <h3>YOUR TITLE HERE (optional)</h3> + <div>YOUR CONTENT HERE</div> + </div></div></div> + <div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div> + </div> + */ + +.x-box-tl { + background: transparent no-repeat 0 0; + zoom:1; +} + +.x-box-tc { + height: 8px; + background: transparent repeat-x 0 0; + overflow: hidden; +} + +.x-box-tr { + background: transparent no-repeat right -8px; +} + +.x-box-ml { + background: transparent repeat-y 0; + padding-left: 4px; + overflow: hidden; + zoom:1; +} + +.x-box-mc { + background: repeat-x 0 -16px; + padding: 4px 10px; +} + +.x-box-mc h3 { + margin: 0 0 4px 0; + zoom:1; +} + +.x-box-mr { + background: transparent repeat-y right; + padding-right: 4px; + overflow: hidden; +} + +.x-box-bl { + background: transparent no-repeat 0 -16px; + zoom:1; +} + +.x-box-bc { + background: transparent repeat-x 0 -8px; + height: 8px; + overflow: hidden; +} + +.x-box-br { + background: transparent no-repeat right -24px; +} + +.x-box-tl, .x-box-bl { + padding-left: 8px; + overflow: hidden; +} + +.x-box-tr, .x-box-br { + padding-right: 8px; + overflow: hidden; +}.x-combo-list { + border:1px solid; + zoom:1; + overflow:hidden; +} + +.x-combo-list-inner { + overflow:auto; + position:relative; /* for calculating scroll offsets */ + zoom:1; + overflow-x:hidden; +} + +.x-combo-list-hd { + border-bottom:1px solid; + padding:3px; +} + +.x-resizable-pinned .x-combo-list-inner { + border-bottom:1px solid; +} + +.x-combo-list-item { + padding:2px; + border:1px solid; + white-space: nowrap; + overflow:hidden; + text-overflow: ellipsis; +} + +.x-combo-list .x-combo-selected{ + border:1px dotted !important; + cursor:pointer; +} + +.x-combo-list .x-toolbar { + border-top:1px solid; + border-bottom:0 none; +}.x-panel { + border-style: solid; + border-width:0; +} + +.x-panel-header { + overflow:hidden; + zoom:1; + padding:5px 3px 4px 5px; + border:1px solid; + line-height: 15px; + background: transparent repeat-x 0 -1px; +} + +.x-panel-body { + border:1px solid; + border-top:0 none; + overflow:hidden; + position: relative; /* added for item scroll positioning */ +} + +.x-panel-bbar .x-toolbar, .x-panel-tbar .x-toolbar { + border:1px solid; + border-top:0 none; + overflow:hidden; + padding:2px; +} + +.x-panel-tbar-noheader .x-toolbar, .x-panel-mc .x-panel-tbar .x-toolbar { + border-top:1px solid; + border-bottom: 0 none; +} + +.x-panel-body-noheader, .x-panel-mc .x-panel-body { + border-top:1px solid; +} + +.x-panel-header { + overflow:hidden; + zoom:1; +} + +.x-panel-tl .x-panel-header { + padding:5px 0 4px 0; + border:0 none; + background:transparent no-repeat; +} + +.x-panel-tl .x-panel-icon, .x-window-tl .x-panel-icon { + padding-left:20px !important; + background-repeat:no-repeat; + background-position:0 4px; + zoom:1; +} + +.x-panel-inline-icon { + width:16px; + height:16px; + background-repeat:no-repeat; + background-position:0 0; + vertical-align:middle; + margin-right:4px; + margin-top:-1px; + margin-bottom:-1px; +} + +.x-panel-tc { + background: transparent repeat-x 0 0; + overflow:hidden; +} + +/* fix ie7 strict mode bug */ +.ext-strict .ext-ie7 .x-panel-tc { + overflow: visible; +} + +.x-panel-tl { + background: transparent no-repeat 0 0; + padding-left:6px; + zoom:1; + border-bottom:1px solid; +} + +.x-panel-tr { + background: transparent no-repeat right 0; + zoom:1; + padding-right:6px; +} + +.x-panel-bc { + background: transparent repeat-x 0 bottom; + zoom:1; +} + +.x-panel-bc .x-panel-footer { + zoom:1; +} + +.x-panel-bl { + background: transparent no-repeat 0 bottom; + padding-left:6px; + zoom:1; +} + +.x-panel-br { + background: transparent no-repeat right bottom; + padding-right:6px; + zoom:1; +} + +.x-panel-mc { + border:0 none; + padding:0; + margin:0; + padding-top:6px; +} + +.x-panel-mc .x-panel-body { + background-color:transparent; + border: 0 none; +} + +.x-panel-ml { + background: repeat-y 0 0; + padding-left:6px; + zoom:1; +} + +.x-panel-mr { + background: transparent repeat-y right 0; + padding-right:6px; + zoom:1; +} + +.x-panel-bc .x-panel-footer { + padding-bottom:6px; +} + +.x-panel-nofooter .x-panel-bc, .x-panel-nofooter .x-window-bc { + height:6px; + font-size:0; + line-height:0; +} + +.x-panel-bwrap { + overflow:hidden; + zoom:1; + left:0; + top:0; +} +.x-panel-body { + overflow:hidden; + zoom:1; +} + +.x-panel-collapsed .x-resizable-handle{ + display:none; +} + +.ext-gecko .x-panel-animated div { + overflow:hidden !important; +} + +/* Plain */ +.x-plain-body { + overflow:hidden; +} + +.x-plain-bbar .x-toolbar { + overflow:hidden; + padding:2px; +} + +.x-plain-tbar .x-toolbar { + overflow:hidden; + padding:2px; +} + +.x-plain-bwrap { + overflow:hidden; + zoom:1; +} + +.x-plain { + overflow:hidden; +} + +/* Tools */ +.x-tool { + overflow:hidden; + width:15px; + height:15px; + float:right; + cursor:pointer; + background:transparent no-repeat; + margin-left:2px; +} + +/* expand / collapse tools */ +.x-tool-toggle { + background-position:0 -60px; +} + +.x-tool-toggle-over { + background-position:-15px -60px; +} + +.x-panel-collapsed .x-tool-toggle { + background-position:0 -75px; +} + +.x-panel-collapsed .x-tool-toggle-over { + background-position:-15px -75px; +} + + +.x-tool-close { + background-position:0 -0; +} + +.x-tool-close-over { + background-position:-15px 0; +} + +.x-tool-minimize { + background-position:0 -15px; +} + +.x-tool-minimize-over { + background-position:-15px -15px; +} + +.x-tool-maximize { + background-position:0 -30px; +} + +.x-tool-maximize-over { + background-position:-15px -30px; +} + +.x-tool-restore { + background-position:0 -45px; +} + +.x-tool-restore-over { + background-position:-15px -45px; +} + +.x-tool-gear { + background-position:0 -90px; +} + +.x-tool-gear-over { + background-position:-15px -90px; +} + +.x-tool-prev { + background-position:0 -105px; +} + +.x-tool-prev-over { + background-position:-15px -105px; +} + +.x-tool-next { + background-position:0 -120px; +} + +.x-tool-next-over { + background-position:-15px -120px; +} + +.x-tool-pin { + background-position:0 -135px; +} + +.x-tool-pin-over { + background-position:-15px -135px; +} + +.x-tool-unpin { + background-position:0 -150px; +} + +.x-tool-unpin-over { + background-position:-15px -150px; +} + +.x-tool-right { + background-position:0 -165px; +} + +.x-tool-right-over { + background-position:-15px -165px; +} + +.x-tool-left { + background-position:0 -180px; +} + +.x-tool-left-over { + background-position:-15px -180px; +} + +.x-tool-down { + background-position:0 -195px; +} + +.x-tool-down-over { + background-position:-15px -195px; +} + +.x-tool-up { + background-position:0 -210px; +} + +.x-tool-up-over { + background-position:-15px -210px; +} + +.x-tool-refresh { + background-position:0 -225px; +} + +.x-tool-refresh-over { + background-position:-15px -225px; +} + +.x-tool-plus { + background-position:0 -240px; +} + +.x-tool-plus-over { + background-position:-15px -240px; +} + +.x-tool-minus { + background-position:0 -255px; +} + +.x-tool-minus-over { + background-position:-15px -255px; +} + +.x-tool-search { + background-position:0 -270px; +} + +.x-tool-search-over { + background-position:-15px -270px; +} + +.x-tool-save { + background-position:0 -285px; +} + +.x-tool-save-over { + background-position:-15px -285px; +} + +.x-tool-help { + background-position:0 -300px; +} + +.x-tool-help-over { + background-position:-15px -300px; +} + +.x-tool-print { + background-position:0 -315px; +} + +.x-tool-print-over { + background-position:-15px -315px; +} + +.x-tool-expand { + background-position:0 -330px; +} + +.x-tool-expand-over { + background-position:-15px -330px; +} + +.x-tool-collapse { + background-position:0 -345px; +} + +.x-tool-collapse-over { + background-position:-15px -345px; +} + +.x-tool-resize { + background-position:0 -360px; +} + +.x-tool-resize-over { + background-position:-15px -360px; +} + +.x-tool-move { + background-position:0 -375px; +} + +.x-tool-move-over { + background-position:-15px -375px; +} + +/* Ghosting */ +.x-panel-ghost { + z-index:12000; + overflow:hidden; + position:absolute; + left:0;top:0; + opacity:.65; + -moz-opacity:.65; + filter:alpha(opacity=65); +} + +.x-panel-ghost ul { + margin:0; + padding:0; + overflow:hidden; + font-size:0; + line-height:0; + border:1px solid; + border-top:0 none; + display:block; +} + +.x-panel-ghost * { + cursor:move !important; +} + +.x-panel-dd-spacer { + border:2px dashed; +} + +/* Buttons */ +.x-panel-btns { + padding:5px; + overflow:hidden; +} + +.x-panel-btns td.x-toolbar-cell{ + padding:3px; +} + +.x-panel-btns .x-btn-focus .x-btn-left{ + background-position:0 -147px; +} + +.x-panel-btns .x-btn-focus .x-btn-right{ + background-position:0 -168px; +} + +.x-panel-btns .x-btn-focus .x-btn-center{ + background-position:0 -189px; +} + +.x-panel-btns .x-btn-over .x-btn-left{ + background-position:0 -63px; +} + +.x-panel-btns .x-btn-over .x-btn-right{ + background-position:0 -84px; +} + +.x-panel-btns .x-btn-over .x-btn-center{ + background-position:0 -105px; +} + +.x-panel-btns .x-btn-click .x-btn-center{ + background-position:0 -126px; +} + +.x-panel-btns .x-btn-click .x-btn-right{ + background-position:0 -84px; +} + +.x-panel-btns .x-btn-click .x-btn-left{ + background-position:0 -63px; +} + +.x-panel-fbar td,.x-panel-fbar span,.x-panel-fbar input,.x-panel-fbar div,.x-panel-fbar select,.x-panel-fbar label{ + white-space: nowrap; +} +/** + * W3C Suggested Default style sheet for HTML 4 + * http://www.w3.org/TR/CSS21/sample.html + * + * Resets for Ext.Panel @cfg normal: true + */ +.x-panel-reset .x-panel-body html, +.x-panel-reset .x-panel-body address, +.x-panel-reset .x-panel-body blockquote, +.x-panel-reset .x-panel-body body, +.x-panel-reset .x-panel-body dd, +.x-panel-reset .x-panel-body div, +.x-panel-reset .x-panel-body dl, +.x-panel-reset .x-panel-body dt, +.x-panel-reset .x-panel-body fieldset, +.x-panel-reset .x-panel-body form, +.x-panel-reset .x-panel-body frame, frameset, +.x-panel-reset .x-panel-body h1, +.x-panel-reset .x-panel-body h2, +.x-panel-reset .x-panel-body h3, +.x-panel-reset .x-panel-body h4, +.x-panel-reset .x-panel-body h5, +.x-panel-reset .x-panel-body h6, +.x-panel-reset .x-panel-body noframes, +.x-panel-reset .x-panel-body ol, +.x-panel-reset .x-panel-body p, +.x-panel-reset .x-panel-body ul, +.x-panel-reset .x-panel-body center, +.x-panel-reset .x-panel-body dir, +.x-panel-reset .x-panel-body hr, +.x-panel-reset .x-panel-body menu, +.x-panel-reset .x-panel-body pre { display: block } +.x-panel-reset .x-panel-body li { display: list-item } +.x-panel-reset .x-panel-body head { display: none } +.x-panel-reset .x-panel-body table { display: table } +.x-panel-reset .x-panel-body tr { display: table-row } +.x-panel-reset .x-panel-body thead { display: table-header-group } +.x-panel-reset .x-panel-body tbody { display: table-row-group } +.x-panel-reset .x-panel-body tfoot { display: table-footer-group } +.x-panel-reset .x-panel-body col { display: table-column } +.x-panel-reset .x-panel-body colgroup { display: table-column-group } +.x-panel-reset .x-panel-body td, +.x-panel-reset .x-panel-body th { display: table-cell } +.x-panel-reset .x-panel-body caption { display: table-caption } +.x-panel-reset .x-panel-body th { font-weight: bolder; text-align: center } +.x-panel-reset .x-panel-body caption { text-align: center } +.x-panel-reset .x-panel-body body { margin: 8px } +.x-panel-reset .x-panel-body h1 { font-size: 2em; margin: .67em 0 } +.x-panel-reset .x-panel-body h2 { font-size: 1.5em; margin: .75em 0 } +.x-panel-reset .x-panel-body h3 { font-size: 1.17em; margin: .83em 0 } +.x-panel-reset .x-panel-body h4, +.x-panel-reset .x-panel-body p, +.x-panel-reset .x-panel-body blockquote, +.x-panel-reset .x-panel-body ul, +.x-panel-reset .x-panel-body fieldset, +.x-panel-reset .x-panel-body form, +.x-panel-reset .x-panel-body ol, +.x-panel-reset .x-panel-body dl, +.x-panel-reset .x-panel-body dir, +.x-panel-reset .x-panel-body menu { margin: 1.12em 0 } +.x-panel-reset .x-panel-body h5 { font-size: .83em; margin: 1.5em 0 } +.x-panel-reset .x-panel-body h6 { font-size: .75em; margin: 1.67em 0 } +.x-panel-reset .x-panel-body h1, +.x-panel-reset .x-panel-body h2, +.x-panel-reset .x-panel-body h3, +.x-panel-reset .x-panel-body h4, +.x-panel-reset .x-panel-body h5, +.x-panel-reset .x-panel-body h6, +.x-panel-reset .x-panel-body b, +.x-panel-reset .x-panel-body strong { font-weight: bolder } +.x-panel-reset .x-panel-body blockquote { margin-left: 40px; margin-right: 40px } +.x-panel-reset .x-panel-body i, +.x-panel-reset .x-panel-body cite, +.x-panel-reset .x-panel-body em, +.x-panel-reset .x-panel-body var, +.x-panel-reset .x-panel-body address { font-style: italic } +.x-panel-reset .x-panel-body pre, +.x-panel-reset .x-panel-body tt, +.x-panel-reset .x-panel-body code, +.x-panel-reset .x-panel-body kbd, +.x-panel-reset .x-panel-body samp { font-family: monospace } +.x-panel-reset .x-panel-body pre { white-space: pre } +.x-panel-reset .x-panel-body button, +.x-panel-reset .x-panel-body textarea, +.x-panel-reset .x-panel-body input, +.x-panel-reset .x-panel-body select { display: inline-block } +.x-panel-reset .x-panel-body big { font-size: 1.17em } +.x-panel-reset .x-panel-body small, +.x-panel-reset .x-panel-body sub, +.x-panel-reset .x-panel-body sup { font-size: .83em } +.x-panel-reset .x-panel-body sub { vertical-align: sub } +.x-panel-reset .x-panel-body sup { vertical-align: super } +.x-panel-reset .x-panel-body table { border-spacing: 2px; } +.x-panel-reset .x-panel-body thead, +.x-panel-reset .x-panel-body tbody, +.x-panel-reset .x-panel-body tfoot { vertical-align: middle } +.x-panel-reset .x-panel-body td, +.x-panel-reset .x-panel-body th { vertical-align: inherit } +.x-panel-reset .x-panel-body s, +.x-panel-reset .x-panel-body strike, +.x-panel-reset .x-panel-body del { text-decoration: line-through } +.x-panel-reset .x-panel-body hr { border: 1px inset } +.x-panel-reset .x-panel-body ol, +.x-panel-reset .x-panel-body ul, +.x-panel-reset .x-panel-body dir, +.x-panel-reset .x-panel-body menu, +.x-panel-reset .x-panel-body dd { margin-left: 40px } +.x-panel-reset .x-panel-body ul, .x-panel-reset .x-panel-body menu, .x-panel-reset .x-panel-body dir { list-style-type: disc;} +.x-panel-reset .x-panel-body ol { list-style-type: decimal } +.x-panel-reset .x-panel-body ol ul, +.x-panel-reset .x-panel-body ul ol, +.x-panel-reset .x-panel-body ul ul, +.x-panel-reset .x-panel-body ol ol { margin-top: 0; margin-bottom: 0 } +.x-panel-reset .x-panel-body u, +.x-panel-reset .x-panel-body ins { text-decoration: underline } +.x-panel-reset .x-panel-body br:before { content: "\A" } +.x-panel-reset .x-panel-body :before, .x-panel-reset .x-panel-body :after { white-space: pre-line } +.x-panel-reset .x-panel-body center { text-align: center } +.x-panel-reset .x-panel-body :link, .x-panel-reset .x-panel-body :visited { text-decoration: underline } +.x-panel-reset .x-panel-body :focus { outline: invert dotted thin } + +/* Begin bidirectionality settings (do not change) */ +.x-panel-reset .x-panel-body BDO[DIR="ltr"] { direction: ltr; unicode-bidi: bidi-override } +.x-panel-reset .x-panel-body BDO[DIR="rtl"] { direction: rtl; unicode-bidi: bidi-override } +.x-window { + zoom:1; +} + +.x-window .x-window-handle { + opacity:0; + -moz-opacity:0; + filter:alpha(opacity=0); +} + +.x-window-proxy { + border:1px solid; + z-index:12000; + overflow:hidden; + position:absolute; + left:0;top:0; + display:none; + opacity:.5; + -moz-opacity:.5; + filter:alpha(opacity=50); +} + +.x-window-header { + overflow:hidden; + zoom:1; +} + +.x-window-bwrap { + z-index:1; + position:relative; + zoom:1; + left:0;top:0; +} + +.x-window-tl .x-window-header { + padding:5px 0 4px 0; +} + +.x-window-header-text { + cursor:pointer; +} + +.x-window-tc { + background: transparent repeat-x 0 0; + overflow:hidden; + zoom:1; +} + +.x-window-tl { + background: transparent no-repeat 0 0; + padding-left:6px; + zoom:1; + z-index:1; + position:relative; +} + +.x-window-tr { + background: transparent no-repeat right 0; + padding-right:6px; +} + +.x-window-bc { + background: transparent repeat-x 0 bottom; + zoom:1; +} + +.x-window-bc .x-window-footer { + padding-bottom:6px; + zoom:1; + font-size:0; + line-height:0; +} + +.x-window-bl { + background: transparent no-repeat 0 bottom; + padding-left:6px; + zoom:1; +} + +.x-window-br { + background: transparent no-repeat right bottom; + padding-right:6px; + zoom:1; +} + +.x-window-mc { + border:1px solid; + padding:0; + margin:0; +} + +.x-window-ml { + background: transparent repeat-y 0 0; + padding-left:6px; + zoom:1; +} + +.x-window-mr { + background: transparent repeat-y right 0; + padding-right:6px; + zoom:1; +} + +.x-window-body { + overflow:hidden; +} + +.x-window-bwrap { + overflow:hidden; +} + +.x-window-maximized .x-window-bl, .x-window-maximized .x-window-br, + .x-window-maximized .x-window-ml, .x-window-maximized .x-window-mr, + .x-window-maximized .x-window-tl, .x-window-maximized .x-window-tr { + padding:0; +} + +.x-window-maximized .x-window-footer { + padding-bottom:0; +} + +.x-window-maximized .x-window-tc { + padding-left:3px; + padding-right:3px; +} + +.x-window-maximized .x-window-mc { + border-left:0 none; + border-right:0 none; +} + +.x-window-tbar .x-toolbar, .x-window-bbar .x-toolbar { + border-left:0 none; + border-right: 0 none; +} + +.x-window-bbar .x-toolbar { + border-top:1px solid; + border-bottom:0 none; +} + +.x-window-draggable, .x-window-draggable .x-window-header-text { + cursor:move; +} + +.x-window-maximized .x-window-draggable, .x-window-maximized .x-window-draggable .x-window-header-text { + cursor:default; +} + +.x-window-body { + background-color:transparent; +} + +.x-panel-ghost .x-window-tl { + border-bottom:1px solid; +} + +.x-panel-collapsed .x-window-tl { + border-bottom:1px solid; +} + +.x-window-maximized-ct { + overflow:hidden; +} + +.x-window-maximized .x-window-handle { + display:none; +} + +.x-window-sizing-ghost ul { + border:0 none !important; +} + +.x-dlg-focus{ + -moz-outline:0 none; + outline:0 none; + width:0; + height:0; + overflow:hidden; + position:absolute; + top:0; + left:0; +} + +.ext-webkit .x-dlg-focus{ + width: 1px; + height: 1px; +} + +.x-dlg-mask{ + z-index:10000; + display:none; + position:absolute; + top:0; + left:0; + -moz-opacity: 0.5; + opacity:.50; + filter: alpha(opacity=50); +} + +body.ext-ie6.x-body-masked select { + visibility:hidden; +} + +body.ext-ie6.x-body-masked .x-window select { + visibility:visible; +} + +.x-window-plain .x-window-mc { + border: 1px solid; +} + +.x-window-plain .x-window-body { + border: 1px solid; + background:transparent !important; +}.x-html-editor-wrap { + border:1px solid; +} + +.x-html-editor-tb .x-btn-text { + background:transparent no-repeat; +} + +.x-html-editor-tb .x-edit-bold, .x-menu-item img.x-edit-bold { + background-position:0 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-italic, .x-menu-item img.x-edit-italic { + background-position:-16px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-underline, .x-menu-item img.x-edit-underline { + background-position:-32px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-forecolor, .x-menu-item img.x-edit-forecolor { + background-position:-160px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-backcolor, .x-menu-item img.x-edit-backcolor { + background-position:-176px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-justifyleft, .x-menu-item img.x-edit-justifyleft { + background-position:-112px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-justifycenter, .x-menu-item img.x-edit-justifycenter { + background-position:-128px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-justifyright, .x-menu-item img.x-edit-justifyright { + background-position:-144px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-insertorderedlist, .x-menu-item img.x-edit-insertorderedlist { + background-position:-80px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-insertunorderedlist, .x-menu-item img.x-edit-insertunorderedlist { + background-position:-96px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-increasefontsize, .x-menu-item img.x-edit-increasefontsize { + background-position:-48px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-decreasefontsize, .x-menu-item img.x-edit-decreasefontsize { + background-position:-64px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-sourceedit, .x-menu-item img.x-edit-sourceedit { + background-position:-192px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tb .x-edit-createlink, .x-menu-item img.x-edit-createlink { + background-position:-208px 0; + background-image:url(../images/default/editor/tb-sprite.gif); +} + +.x-html-editor-tip .x-tip-bd .x-tip-bd-inner { + padding:5px; + padding-bottom:1px; +} + +.x-html-editor-tb .x-toolbar { + position:static !important; +}.x-panel-noborder .x-panel-body-noborder { + border-width:0; +} + +.x-panel-noborder .x-panel-header-noborder { + border-width:0 0 1px; + border-style:solid; +} + +.x-panel-noborder .x-panel-tbar-noborder .x-toolbar { + border-width:0 0 1px; + border-style:solid; +} + +.x-panel-noborder .x-panel-bbar-noborder .x-toolbar { + border-width:1px 0 0 0; + border-style:solid; +} + +.x-window-noborder .x-window-mc { + border-width:0; +} + +.x-window-plain .x-window-body-noborder { + border-width:0; +} + +.x-tab-panel-noborder .x-tab-panel-body-noborder { + border-width:0; +} + +.x-tab-panel-noborder .x-tab-panel-header-noborder { + border-width: 0 0 1px 0; +} + +.x-tab-panel-noborder .x-tab-panel-footer-noborder { + border-width: 1px 0 0 0; +} + +.x-tab-panel-bbar-noborder .x-toolbar { + border-width: 1px 0 0 0; + border-style:solid; +} + +.x-tab-panel-tbar-noborder .x-toolbar { + border-width:0 0 1px; + border-style:solid; +}.x-border-layout-ct { + position: relative; +} + +.x-border-panel { + position:absolute; + left:0; + top:0; +} + +.x-tool-collapse-south { + background-position:0 -195px; +} + +.x-tool-collapse-south-over { + background-position:-15px -195px; +} + +.x-tool-collapse-north { + background-position:0 -210px; +} + +.x-tool-collapse-north-over { + background-position:-15px -210px; +} + +.x-tool-collapse-west { + background-position:0 -180px; +} + +.x-tool-collapse-west-over { + background-position:-15px -180px; +} + +.x-tool-collapse-east { + background-position:0 -165px; +} + +.x-tool-collapse-east-over { + background-position:-15px -165px; +} + +.x-tool-expand-south { + background-position:0 -210px; +} + +.x-tool-expand-south-over { + background-position:-15px -210px; +} + +.x-tool-expand-north { + background-position:0 -195px; +} +.x-tool-expand-north-over { + background-position:-15px -195px; +} + +.x-tool-expand-west { + background-position:0 -165px; +} + +.x-tool-expand-west-over { + background-position:-15px -165px; +} + +.x-tool-expand-east { + background-position:0 -180px; +} + +.x-tool-expand-east-over { + background-position:-15px -180px; +} + +.x-tool-expand-north, .x-tool-expand-south { + float:right; + margin:3px; +} + +.x-tool-expand-east, .x-tool-expand-west { + float:none; + margin:3px 2px; +} + +.x-accordion-hd .x-tool-toggle { + background-position:0 -255px; +} + +.x-accordion-hd .x-tool-toggle-over { + background-position:-15px -255px; +} + +.x-panel-collapsed .x-accordion-hd .x-tool-toggle { + background-position:0 -240px; +} + +.x-panel-collapsed .x-accordion-hd .x-tool-toggle-over { + background-position:-15px -240px; +} + +.x-accordion-hd { + padding-top:4px; + padding-bottom:3px; + border-top:0 none; + background: transparent repeat-x 0 -9px; +} + +.x-layout-collapsed{ + position:absolute; + left:-10000px; + top:-10000px; + visibility:hidden; + width:20px; + height:20px; + overflow:hidden; + border:1px solid; + z-index:20; +} + +.ext-border-box .x-layout-collapsed{ + width:22px; + height:22px; +} + +.x-layout-collapsed-over{ + cursor:pointer; +} + +.x-layout-collapsed-west .x-layout-collapsed-tools, .x-layout-collapsed-east .x-layout-collapsed-tools{ + position:absolute; + top:0; + left:0; + width:20px; + height:20px; +} + + +.x-layout-split{ + position:absolute; + height:5px; + width:5px; + line-height:1px; + font-size:1px; + z-index:3; + background-color:transparent; +} + +/* IE6 strict won't drag w/out a color */ +.ext-strict .ext-ie6 .x-layout-split{ + background-color: #fff !important; + filter: alpha(opacity=1); +} + +.x-layout-split-h{ + background-image:url(../images/default/s.gif); + background-position: left; +} + +.x-layout-split-v{ + background-image:url(../images/default/s.gif); + background-position: top; +} + +.x-column-layout-ct { + overflow:hidden; + zoom:1; +} + +.x-column { + float:left; + padding:0; + margin:0; + overflow:hidden; + zoom:1; +} + +.x-column-inner { + overflow:hidden; + zoom:1; +} + +/* mini mode */ +.x-layout-mini { + position:absolute; + top:0; + left:0; + display:block; + width:5px; + height:35px; + cursor:pointer; + opacity:.5; + -moz-opacity:.5; + filter:alpha(opacity=50); +} + +.x-layout-mini-over, .x-layout-collapsed-over .x-layout-mini{ + opacity:1; + -moz-opacity:1; + filter:none; +} + +.x-layout-split-west .x-layout-mini { + top:48%; +} + +.x-layout-split-east .x-layout-mini { + top:48%; +} + +.x-layout-split-north .x-layout-mini { + left:48%; + height:5px; + width:35px; +} + +.x-layout-split-south .x-layout-mini { + left:48%; + height:5px; + width:35px; +} + +.x-layout-cmini-west .x-layout-mini { + top:48%; +} + +.x-layout-cmini-east .x-layout-mini { + top:48%; +} + +.x-layout-cmini-north .x-layout-mini { + left:48%; + height:5px; + width:35px; +} + +.x-layout-cmini-south .x-layout-mini { + left:48%; + height:5px; + width:35px; +} + +.x-layout-cmini-west, .x-layout-cmini-east { + border:0 none; + width:5px !important; + padding:0; + background-color:transparent; +} + +.x-layout-cmini-north, .x-layout-cmini-south { + border:0 none; + height:5px !important; + padding:0; + background-color:transparent; +} + +.x-viewport, .x-viewport body { + margin: 0; + padding: 0; + border: 0 none; + overflow: hidden; + height: 100%; +} + +.x-abs-layout-item { + position:absolute; + left:0; + top:0; +} + +.ext-ie input.x-abs-layout-item, .ext-ie textarea.x-abs-layout-item { + margin:0; +} + +.x-box-layout-ct { + overflow:hidden; + zoom:1; +} + +.x-box-inner { + overflow:hidden; + zoom:1; + position:relative; + left:0; + top:0; +} + +.x-box-item { + position:absolute; + left:0; + top:0; +}.x-progress-wrap { + border:1px solid; + overflow:hidden; +} + +.x-progress-inner { + height:18px; + background:repeat-x; + position:relative; +} + +.x-progress-bar { + height:18px; + float:left; + width:0; + background: repeat-x left center; + border-top:1px solid; + border-bottom:1px solid; + border-right:1px solid; +} + +.x-progress-text { + padding:1px 5px; + overflow:hidden; + position:absolute; + left:0; + text-align:center; +} + +.x-progress-text-back { + line-height:16px; +} + +.ext-ie .x-progress-text-back { + line-height:15px; +} + +.ext-strict .ext-ie7 .x-progress-text-back{ + width: 100%; +} +.x-list-header{ + background: repeat-x 0 bottom; + cursor:default; + zoom:1; + height:22px; +} + +.x-list-header-inner div { + display:block; + float:left; + overflow:hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} + +.x-list-header-inner div em { + display:block; + border-left:1px solid; + padding:4px 4px; + overflow:hidden; + -moz-user-select: none; + -khtml-user-select: none; + line-height:14px; +} + +.x-list-body { + overflow:auto; + overflow-x:hidden; + overflow-y:auto; + zoom:1; + float: left; + width: 100%; +} + +.x-list-body dl { + zoom:1; +} + +.x-list-body dt { + display:block; + float:left; + overflow:hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; + cursor:pointer; + zoom:1; +} + +.x-list-body dt em { + display:block; + padding:3px 4px; + overflow:hidden; + -moz-user-select: none; + -khtml-user-select: none; +} + +.x-list-resizer { + border-left:1px solid; + border-right:1px solid; + position:absolute; + left:0; + top:0; +} + +.x-list-header-inner em.sort-asc { + background: transparent no-repeat center 0; + border-style:solid; + border-width: 0 1px 1px; + padding-bottom:3px; +} + +.x-list-header-inner em.sort-desc { + background: transparent no-repeat center -23px; + border-style:solid; + border-width: 0 1px 1px; + padding-bottom:3px; +} + +/* Shared styles */ +.x-slider { + zoom:1; +} + +.x-slider-inner { + position:relative; + left:0; + top:0; + overflow:visible; + zoom:1; +} + +.x-slider-focus { + position:absolute; + left:0; + top:0; + width:1px; + height:1px; + line-height:1px; + font-size:1px; + -moz-outline:0 none; + outline:0 none; + -moz-user-select: none; + -khtml-user-select:none; + -webkit-user-select:ignore; + display:block; + overflow:hidden; +} + +/* Horizontal styles */ +.x-slider-horz { + padding-left:7px; + background:transparent no-repeat 0 -22px; +} + +.x-slider-horz .x-slider-end { + padding-right:7px; + zoom:1; + background:transparent no-repeat right -44px; +} + +.x-slider-horz .x-slider-inner { + background:transparent repeat-x 0 0; + height:22px; +} + +.x-slider-horz .x-slider-thumb { + width:14px; + height:15px; + position:absolute; + left:0; + top:3px; + background:transparent no-repeat 0 0; +} + +.x-slider-horz .x-slider-thumb-over { + background-position: -14px -15px; +} + +.x-slider-horz .x-slider-thumb-drag { + background-position: -28px -30px; +} + +/* Vertical styles */ +.x-slider-vert { + padding-top:7px; + background:transparent no-repeat -44px 0; + width:22px; +} + +.x-slider-vert .x-slider-end { + padding-bottom:7px; + zoom:1; + background:transparent no-repeat -22px bottom; +} + +.x-slider-vert .x-slider-inner { + background:transparent repeat-y 0 0; +} + +.x-slider-vert .x-slider-thumb { + width:15px; + height:14px; + position:absolute; + left:3px; + bottom:0; + background:transparent no-repeat 0 0; +} + +.x-slider-vert .x-slider-thumb-over { + background-position: -15px -14px; +} + +.x-slider-vert .x-slider-thumb-drag { + background-position: -30px -28px; +}.x-window-dlg .x-window-body { + border:0 none !important; + padding:5px 10px; + overflow:hidden !important; +} + +.x-window-dlg .x-window-mc { + border:0 none !important; +} + +.x-window-dlg .ext-mb-input { + margin-top:4px; + width:95%; +} + +.x-window-dlg .ext-mb-textarea { + margin-top:4px; +} + +.x-window-dlg .x-progress-wrap { + margin-top:4px; +} + +.ext-ie .x-window-dlg .x-progress-wrap { + margin-top:6px; +} + +.x-window-dlg .x-msg-box-wait { + background:transparent no-repeat left; + display:block; + width:300px; + padding-left:18px; + line-height:18px; +} + +.x-window-dlg .ext-mb-icon { + float:left; + width:47px; + height:32px; +} + +.x-window-dlg .x-dlg-icon .ext-mb-content{ + zoom: 1; + margin-left: 47px; +} + +.x-window-dlg .ext-mb-info, .x-window-dlg .ext-mb-warning, .x-window-dlg .ext-mb-question, .x-window-dlg .ext-mb-error { + background:transparent no-repeat top left; +} + +.ext-gecko2 .ext-mb-fix-cursor { + overflow:auto; +}.ext-el-mask { + background-color: #ccc; +} + +.ext-el-mask-msg { + border-color:#6593cf; + background-color:#c3daf9; + background-image:url(../images/default/box/tb-blue.gif); +} +.ext-el-mask-msg div { + background-color: #eee; + border-color:#a3bad9; + color:#222; + font:normal 11px tahoma, arial, helvetica, sans-serif; +} + +.x-mask-loading div { + background-color:#fbfbfb; + background-image:url(../images/default/grid/loading.gif); +} + +.x-item-disabled { + color: gray; +} + +.x-item-disabled * { + color: gray !important; +} + +.x-splitbar-proxy { + background-color: #aaa; +} + +.x-color-palette a { + border-color:#fff; +} + +.x-color-palette a:hover, .x-color-palette a.x-color-palette-sel { + border-color:#8bb8f3; + background-color: #deecfd; +} + +/* +.x-color-palette em:hover, .x-color-palette span:hover{ + background-color: #deecfd; +} +*/ + +.x-color-palette em { + border-color:#aca899; +} + +.x-ie-shadow { + background-color:#777; +} + +.x-shadow .xsmc { + background-image: url(../images/default/shadow-c.png); +} + +.x-shadow .xsml, .x-shadow .xsmr { + background-image: url(../images/default/shadow-lr.png); +} + +.x-shadow .xstl, .x-shadow .xstc, .x-shadow .xstr, .x-shadow .xsbl, .x-shadow .xsbc, .x-shadow .xsbr{ + background-image: url(../images/default/shadow.png); +} + +.loading-indicator { + font-size: 11px; + background-image: url(../images/default/grid/loading.gif); +} + +.x-spotlight { + background-color: #ccc; +} +.x-tab-panel-header, .x-tab-panel-footer { + background-color: #deecfd; + border-color:#8db2e3; + overflow:hidden; + zoom:1; +} + +.x-tab-panel-header, .x-tab-panel-footer { + border-color:#8db2e3; +} + +ul.x-tab-strip-top{ + background-color:#cedff5; + background-image: url(../images/default/tabs/tab-strip-bg.gif); + border-bottom-color:#8db2e3; +} + +ul.x-tab-strip-bottom{ + background-color:#cedff5; + background-image: url(../images/default/tabs/tab-strip-btm-bg.gif); + border-top-color:#8db2e3; +} + +.x-tab-panel-header-plain .x-tab-strip-spacer, +.x-tab-panel-footer-plain .x-tab-strip-spacer { + border-color:#8db2e3; + background-color: #deecfd; +} + +.x-tab-strip span.x-tab-strip-text { + font:normal 11px tahoma,arial,helvetica; + color:#416aa3; +} + +.x-tab-strip-over span.x-tab-strip-text { + color:#15428b; +} + +.x-tab-strip-active span.x-tab-strip-text { + color:#15428b; + font-weight:bold; +} + +.x-tab-strip-disabled .x-tabs-text { + color:#aaaaaa; +} + +.x-tab-strip-top .x-tab-right, .x-tab-strip-top .x-tab-left, .x-tab-strip-top .x-tab-strip-inner{ + background-image: url(../images/default/tabs/tabs-sprite.gif); +} + +.x-tab-strip-bottom .x-tab-right { + background-image: url(../images/default/tabs/tab-btm-inactive-right-bg.gif); +} + +.x-tab-strip-bottom .x-tab-left { + background-image: url(../images/default/tabs/tab-btm-inactive-left-bg.gif); +} + +.x-tab-strip-bottom .x-tab-strip-over .x-tab-right { + background-image: url(../images/default/tabs/tab-btm-over-right-bg.gif); +} + +.x-tab-strip-bottom .x-tab-strip-over .x-tab-left { + background-image: url(../images/default/tabs/tab-btm-over-left-bg.gif); +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-right { + background-image: url(../images/default/tabs/tab-btm-right-bg.gif); +} + +.x-tab-strip-bottom .x-tab-strip-active .x-tab-left { + background-image: url(../images/default/tabs/tab-btm-left-bg.gif); +} + +.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close { + background-image:url(../images/default/tabs/tab-close.gif); +} + +.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close:hover{ + background-image:url(../images/default/tabs/tab-close.gif); +} + +.x-tab-panel-body { + border-color:#8db2e3; + background-color:#fff; +} + +.x-tab-panel-body-top { + border-top: 0 none; +} + +.x-tab-panel-body-bottom { + border-bottom: 0 none; +} + +.x-tab-scroller-left { + background-image:url(../images/default/tabs/scroll-left.gif); + border-bottom-color:#8db2e3; +} + +.x-tab-scroller-left-over { + background-position: 0 0; +} + +.x-tab-scroller-left-disabled { + background-position: -18px 0; + opacity:.5; + -moz-opacity:.5; + filter:alpha(opacity=50); + cursor:default; +} + +.x-tab-scroller-right { + background-image:url(../images/default/tabs/scroll-right.gif); + border-bottom-color:#8db2e3; +} + +.x-tab-panel-bbar .x-toolbar, .x-tab-panel-tbar .x-toolbar { + border-color:#99bbe8; +}.x-form-field { + font:normal 12px tahoma, arial, helvetica, sans-serif; +} + +.x-form-text, textarea.x-form-field { + background-color:#fff; + background-image:url(../images/default/form/text-bg.gif); + border-color:#b5b8c8; +} + +.x-form-select-one { + background-color:#fff; + border-color:#b5b8c8; +} + +.x-form-check-group-label { + border-bottom: 1px solid #99bbe8; + color: #15428b; +} + +.x-editor .x-form-check-wrap { + background-color:#fff; +} + +.x-form-field-wrap .x-form-trigger { + background-image:url(../images/default/form/trigger.gif); + border-bottom-color:#b5b8c8; +} + +.x-form-field-wrap .x-form-date-trigger { + background-image: url(../images/default/form/date-trigger.gif); +} + +.x-form-field-wrap .x-form-clear-trigger { + background-image: url(../images/default/form/clear-trigger.gif); +} + +.x-form-field-wrap .x-form-search-trigger { + background-image: url(../images/default/form/search-trigger.gif); +} + +.x-trigger-wrap-focus .x-form-trigger { + border-bottom-color:#7eadd9; +} + +.x-item-disabled .x-form-trigger-over { + border-bottom-color:#b5b8c8; +} + +.x-item-disabled .x-form-trigger-click { + border-bottom-color:#b5b8c8; +} + +.x-form-focus, textarea.x-form-focus { + border-color:#7eadd9; +} + +.x-form-invalid, textarea.x-form-invalid { + background-color:#fff; + background-image:url(../images/default/grid/invalid_line.gif); + border-color:#c30; +} + +.x-form-invalid.x-form-composite { + border: none; + background-image: none; +} + +.x-form-invalid.x-form-composite .x-form-invalid { + background-color:#fff; + background-image:url(../images/default/grid/invalid_line.gif); + border-color:#c30; +} + +.x-form-inner-invalid, textarea.x-form-inner-invalid { + background-color:#fff; + background-image:url(../images/default/grid/invalid_line.gif); +} + +.x-form-grow-sizer { + font:normal 12px tahoma, arial, helvetica, sans-serif; +} + +.x-form-item { + font:normal 12px tahoma, arial, helvetica, sans-serif; +} + +.x-form-invalid-msg { + color:#c0272b; + font:normal 11px tahoma, arial, helvetica, sans-serif; + background-image:url(../images/default/shared/warning.gif); +} + +.x-form-empty-field { + color:gray; +} + +.x-small-editor .x-form-field { + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.ext-webkit .x-small-editor .x-form-field { + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-form-invalid-icon { + background-image:url(../images/default/form/exclamation.gif); +} + +.x-fieldset { + border-color:#b5b8c8; +} + +.x-fieldset legend { + font:bold 11px tahoma, arial, helvetica, sans-serif; + color:#15428b; +} +.x-btn{ + font:normal 11px tahoma, verdana, helvetica; +} + +.x-btn button{ + font:normal 11px arial,tahoma,verdana,helvetica; + color:#333; +} + +.x-btn em { + font-style:normal; + font-weight:normal; +} + +.x-btn-tl, .x-btn-tr, .x-btn-tc, .x-btn-ml, .x-btn-mr, .x-btn-mc, .x-btn-bl, .x-btn-br, .x-btn-bc{ + background-image:url(../images/default/button/btn.gif); +} + +.x-btn-click .x-btn-text, .x-btn-menu-active .x-btn-text, .x-btn-pressed .x-btn-text{ + color:#000; +} + +.x-btn-disabled *{ + color:gray !important; +} + +.x-btn-mc em.x-btn-arrow { + background-image:url(../images/default/button/arrow.gif); +} + +.x-btn-mc em.x-btn-split { + background-image:url(../images/default/button/s-arrow.gif); +} + +.x-btn-over .x-btn-mc em.x-btn-split, .x-btn-click .x-btn-mc em.x-btn-split, .x-btn-menu-active .x-btn-mc em.x-btn-split, .x-btn-pressed .x-btn-mc em.x-btn-split { + background-image:url(../images/default/button/s-arrow-o.gif); +} + +.x-btn-mc em.x-btn-arrow-bottom { + background-image:url(../images/default/button/s-arrow-b-noline.gif); +} + +.x-btn-mc em.x-btn-split-bottom { + background-image:url(../images/default/button/s-arrow-b.gif); +} + +.x-btn-over .x-btn-mc em.x-btn-split-bottom, .x-btn-click .x-btn-mc em.x-btn-split-bottom, .x-btn-menu-active .x-btn-mc em.x-btn-split-bottom, .x-btn-pressed .x-btn-mc em.x-btn-split-bottom { + background-image:url(../images/default/button/s-arrow-bo.gif); +} + +.x-btn-group-header { + color: #3e6aaa; +} + +.x-btn-group-tc { + background-image: url(../images/default/button/group-tb.gif); +} + +.x-btn-group-tl { + background-image: url(../images/default/button/group-cs.gif); +} + +.x-btn-group-tr { + background-image: url(../images/default/button/group-cs.gif); +} + +.x-btn-group-bc { + background-image: url(../images/default/button/group-tb.gif); +} + +.x-btn-group-bl { + background-image: url(../images/default/button/group-cs.gif); +} + +.x-btn-group-br { + background-image: url(../images/default/button/group-cs.gif); +} + +.x-btn-group-ml { + background-image: url(../images/default/button/group-lr.gif); +} +.x-btn-group-mr { + background-image: url(../images/default/button/group-lr.gif); +} + +.x-btn-group-notitle .x-btn-group-tc { + background-image: url(../images/default/button/group-tb.gif); +}.x-toolbar{ + border-color:#a9bfd3; + background-color:#d0def0; + background-image:url(../images/default/toolbar/bg.gif); +} + +.x-toolbar td,.x-toolbar span,.x-toolbar input,.x-toolbar div,.x-toolbar select,.x-toolbar label{ + font:normal 11px arial,tahoma, helvetica, sans-serif; +} + +.x-toolbar .x-item-disabled { + color:gray; +} + +.x-toolbar .x-item-disabled * { + color:gray; +} + +.x-toolbar .x-btn-mc em.x-btn-split { + background-image:url(../images/default/button/s-arrow-noline.gif); +} + +.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split, .x-toolbar .x-btn-click .x-btn-mc em.x-btn-split, +.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split, .x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split +{ + background-image:url(../images/default/button/s-arrow-o.gif); +} + +.x-toolbar .x-btn-mc em.x-btn-split-bottom { + background-image:url(../images/default/button/s-arrow-b-noline.gif); +} + +.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split-bottom, .x-toolbar .x-btn-click .x-btn-mc em.x-btn-split-bottom, +.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split-bottom, .x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split-bottom +{ + background-image:url(../images/default/button/s-arrow-bo.gif); +} + +.x-toolbar .xtb-sep { + background-image: url(../images/default/grid/grid-blue-split.gif); +} + +.x-tbar-page-first{ + background-image: url(../images/default/grid/page-first.gif) !important; +} + +.x-tbar-loading{ + background-image: url(../images/default/grid/refresh.gif) !important; +} + +.x-tbar-page-last{ + background-image: url(../images/default/grid/page-last.gif) !important; +} + +.x-tbar-page-next{ + background-image: url(../images/default/grid/page-next.gif) !important; +} + +.x-tbar-page-prev{ + background-image: url(../images/default/grid/page-prev.gif) !important; +} + +.x-item-disabled .x-tbar-loading{ + background-image: url(../images/default/grid/refresh-disabled.gif) !important; +} + +.x-item-disabled .x-tbar-page-first{ + background-image: url(../images/default/grid/page-first-disabled.gif) !important; +} + +.x-item-disabled .x-tbar-page-last{ + background-image: url(../images/default/grid/page-last-disabled.gif) !important; +} + +.x-item-disabled .x-tbar-page-next{ + background-image: url(../images/default/grid/page-next-disabled.gif) !important; +} + +.x-item-disabled .x-tbar-page-prev{ + background-image: url(../images/default/grid/page-prev-disabled.gif) !important; +} + +.x-paging-info { + color:#444; +} + +.x-toolbar-more-icon { + background-image: url(../images/default/toolbar/more.gif) !important; +}.x-resizable-handle { + background-color:#fff; +} + +.x-resizable-over .x-resizable-handle-east, .x-resizable-pinned .x-resizable-handle-east, +.x-resizable-over .x-resizable-handle-west, .x-resizable-pinned .x-resizable-handle-west +{ + background-image:url(../images/default/sizer/e-handle.gif); +} + +.x-resizable-over .x-resizable-handle-south, .x-resizable-pinned .x-resizable-handle-south, +.x-resizable-over .x-resizable-handle-north, .x-resizable-pinned .x-resizable-handle-north +{ + background-image:url(../images/default/sizer/s-handle.gif); +} + +.x-resizable-over .x-resizable-handle-north, .x-resizable-pinned .x-resizable-handle-north{ + background-image:url(../images/default/sizer/s-handle.gif); +} +.x-resizable-over .x-resizable-handle-southeast, .x-resizable-pinned .x-resizable-handle-southeast{ + background-image:url(../images/default/sizer/se-handle.gif); +} +.x-resizable-over .x-resizable-handle-northwest, .x-resizable-pinned .x-resizable-handle-northwest{ + background-image:url(../images/default/sizer/nw-handle.gif); +} +.x-resizable-over .x-resizable-handle-northeast, .x-resizable-pinned .x-resizable-handle-northeast{ + background-image:url(../images/default/sizer/ne-handle.gif); +} +.x-resizable-over .x-resizable-handle-southwest, .x-resizable-pinned .x-resizable-handle-southwest{ + background-image:url(../images/default/sizer/sw-handle.gif); +} +.x-resizable-proxy{ + border-color:#3b5a82; +} +.x-resizable-overlay{ + background-color:#fff; +} +.x-grid3 { + background-color:#fff; +} + +.x-grid-panel .x-panel-mc .x-panel-body { + border-color:#99bbe8; +} + +.x-grid3-row td, .x-grid3-summary-row td{ + font:normal 11px/13px arial, tahoma, helvetica, sans-serif; +} + +.x-grid3-hd-row td { + font:normal 11px/15px arial, tahoma, helvetica, sans-serif; +} + + +.x-grid3-hd-row td { + border-left-color:#eee; + border-right-color:#d0d0d0; +} + +.x-grid-row-loading { + background-color: #fff; + background-image:url(../images/default/shared/loading-balls.gif); +} + +.x-grid3-row { + border-color:#ededed; + border-top-color:#fff; +} + +.x-grid3-row-alt{ + background-color:#fafafa; +} + +.x-grid3-row-over { + border-color:#ddd; + background-color:#efefef; + background-image:url(../images/default/grid/row-over.gif); +} + +.x-grid3-resize-proxy { + background-color:#777; +} + +.x-grid3-resize-marker { + background-color:#777; +} + +.x-grid3-header{ + background-color:#f9f9f9; + background-image:url(../images/default/grid/grid3-hrow.gif); +} + +.x-grid3-header-pop { + border-left-color:#d0d0d0; +} + +.x-grid3-header-pop-inner { + border-left-color:#eee; + background-image:url(../images/default/grid/hd-pop.gif); +} + +td.x-grid3-hd-over, td.sort-desc, td.sort-asc, td.x-grid3-hd-menu-open { + border-left-color:#aaccf6; + border-right-color:#aaccf6; +} + +td.x-grid3-hd-over .x-grid3-hd-inner, td.sort-desc .x-grid3-hd-inner, td.sort-asc .x-grid3-hd-inner, td.x-grid3-hd-menu-open .x-grid3-hd-inner { + background-color:#ebf3fd; + background-image:url(../images/default/grid/grid3-hrow-over.gif); + +} + +.sort-asc .x-grid3-sort-icon { + background-image: url(../images/default/grid/sort_asc.gif); +} + +.sort-desc .x-grid3-sort-icon { + background-image: url(../images/default/grid/sort_desc.gif); +} + +.x-grid3-cell-text, .x-grid3-hd-text { + color:#000; +} + +.x-grid3-split { + background-image: url(../images/default/grid/grid-split.gif); +} + +.x-grid3-hd-text { + color:#15428b; +} + +.x-dd-drag-proxy .x-grid3-hd-inner{ + background-color:#ebf3fd; + background-image:url(../images/default/grid/grid3-hrow-over.gif); + border-color:#aaccf6; +} + +.col-move-top{ + background-image:url(../images/default/grid/col-move-top.gif); +} + +.col-move-bottom{ + background-image:url(../images/default/grid/col-move-bottom.gif); +} + +td.grid-hd-group-cell { + background: url(../images/default/grid/grid3-hrow.gif) repeat-x bottom; +} + +.x-grid3-row-selected { + background-color: #dfe8f6 !important; + background-image: none; + border-color:#a3bae9; +} + +.x-grid3-cell-selected{ + background-color: #b8cfee !important; + color:#000; +} + +.x-grid3-cell-selected span{ + color:#000 !important; +} + +.x-grid3-cell-selected .x-grid3-cell-text{ + color:#000; +} + +.x-grid3-locked td.x-grid3-row-marker, .x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker{ + background-color:#ebeadb !important; + background-image:url(../images/default/grid/grid-hrow.gif) !important; + color:#000; + border-top-color:#fff; + border-right-color:#6fa0df !important; +} + +.x-grid3-locked td.x-grid3-row-marker div, .x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker div{ + color:#15428b !important; +} + +.x-grid3-dirty-cell { + background-image:url(../images/default/grid/dirty.gif); +} + +.x-grid3-topbar, .x-grid3-bottombar{ + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-grid3-bottombar .x-toolbar{ + border-top-color:#a9bfd3; +} + +.x-props-grid .x-grid3-td-name .x-grid3-cell-inner{ + background-image:url(../images/default/grid/grid3-special-col-bg.gif) !important; + color:#000 !important; +} + +.x-props-grid .x-grid3-body .x-grid3-td-name{ + background-color:#fff !important; + border-right-color:#eee; +} + +.xg-hmenu-sort-asc .x-menu-item-icon{ + background-image: url(../images/default/grid/hmenu-asc.gif); +} + +.xg-hmenu-sort-desc .x-menu-item-icon{ + background-image: url(../images/default/grid/hmenu-desc.gif); +} + +.xg-hmenu-lock .x-menu-item-icon{ + background-image: url(../images/default/grid/hmenu-lock.gif); +} + +.xg-hmenu-unlock .x-menu-item-icon{ + background-image: url(../images/default/grid/hmenu-unlock.gif); +} + +.x-grid3-hd-btn { + background-color:#c3daf9; + background-image:url(../images/default/grid/grid3-hd-btn.gif); +} + +.x-grid3-body .x-grid3-td-expander { + background-image:url(../images/default/grid/grid3-special-col-bg.gif); +} + +.x-grid3-row-expander { + background-image:url(../images/default/grid/row-expand-sprite.gif); +} + +.x-grid3-body .x-grid3-td-checker { + background-image: url(../images/default/grid/grid3-special-col-bg.gif); +} + +.x-grid3-row-checker, .x-grid3-hd-checker { + background-image:url(../images/default/grid/row-check-sprite.gif); +} + +.x-grid3-body .x-grid3-td-numberer { + background-image:url(../images/default/grid/grid3-special-col-bg.gif); +} + +.x-grid3-body .x-grid3-td-numberer .x-grid3-cell-inner { + color:#444; +} + +.x-grid3-body .x-grid3-td-row-icon { + background-image:url(../images/default/grid/grid3-special-col-bg.gif); +} + +.x-grid3-body .x-grid3-row-selected .x-grid3-td-numberer, +.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker, +.x-grid3-body .x-grid3-row-selected .x-grid3-td-expander { + background-image:url(../images/default/grid/grid3-special-col-sel-bg.gif); +} + +.x-grid3-check-col { + background-image:url(../images/default/menu/unchecked.gif); +} + +.x-grid3-check-col-on { + background-image:url(../images/default/menu/checked.gif); +} + +.x-grid-group, .x-grid-group-body, .x-grid-group-hd { + zoom:1; +} + +.x-grid-group-hd { + border-bottom-color:#99bbe8; +} + +.x-grid-group-hd div.x-grid-group-title { + background-image:url(../images/default/grid/group-collapse.gif); + color:#3764a0; + font:bold 11px tahoma, arial, helvetica, sans-serif; +} + +.x-grid-group-collapsed .x-grid-group-hd div.x-grid-group-title { + background-image:url(../images/default/grid/group-expand.gif); +} + +.x-group-by-icon { + background-image:url(../images/default/grid/group-by.gif); +} + +.x-cols-icon { + background-image:url(../images/default/grid/columns.gif); +} + +.x-show-groups-icon { + background-image:url(../images/default/grid/group-by.gif); +} + +.x-grid-empty { + color:gray; + font:normal 11px tahoma, arial, helvetica, sans-serif; +} + +.x-grid-with-col-lines .x-grid3-row td.x-grid3-cell { + border-right-color:#ededed; +} + +.x-grid-with-col-lines .x-grid3-row-selected { + border-top-color:#a3bae9; +}.x-pivotgrid .x-grid3-header-offset table td { + background: url(../images/default/grid/grid3-hrow.gif) repeat-x 50% 100%; + border-left: 1px solid; + border-right: 1px solid; + border-left-color: #EEE; + border-right-color: #D0D0D0; +} + +.x-pivotgrid .x-grid3-row-headers { + background-color: #f9f9f9; +} + +.x-pivotgrid .x-grid3-row-headers table td { + background: #EEE url(../images/default/grid/grid3-rowheader.gif) repeat-x left top; + border-left: 1px solid; + border-right: 1px solid; + border-left-color: #EEE; + border-right-color: #D0D0D0; + border-bottom: 1px solid; + border-bottom-color: #D0D0D0; + height: 18px; +} +.x-dd-drag-ghost{ + color:#000; + font: normal 11px arial, helvetica, sans-serif; + border-color: #ddd #bbb #bbb #ddd; + background-color:#fff; +} + +.x-dd-drop-nodrop .x-dd-drop-icon{ + background-image: url(../images/default/dd/drop-no.gif); +} + +.x-dd-drop-ok .x-dd-drop-icon{ + background-image: url(../images/default/dd/drop-yes.gif); +} + +.x-dd-drop-ok-add .x-dd-drop-icon{ + background-image: url(../images/default/dd/drop-add.gif); +} + +.x-view-selector { + background-color:#c3daf9; + border-color:#3399bb; +}.x-tree-node-expanded .x-tree-node-icon{ + background-image:url(../images/default/tree/folder-open.gif); +} + +.x-tree-node-leaf .x-tree-node-icon{ + background-image:url(../images/default/tree/leaf.gif); +} + +.x-tree-node-collapsed .x-tree-node-icon{ + background-image:url(../images/default/tree/folder.gif); +} + +.x-tree-node-loading .x-tree-node-icon{ + background-image:url(../images/default/tree/loading.gif) !important; +} + +.x-tree-node .x-tree-node-inline-icon { + background-image: none; +} + +.x-tree-node-loading a span{ + font-style: italic; + color:#444444; +} + +.x-tree-lines .x-tree-elbow{ + background-image:url(../images/default/tree/elbow.gif); +} + +.x-tree-lines .x-tree-elbow-plus{ + background-image:url(../images/default/tree/elbow-plus.gif); +} + +.x-tree-lines .x-tree-elbow-minus{ + background-image:url(../images/default/tree/elbow-minus.gif); +} + +.x-tree-lines .x-tree-elbow-end{ + background-image:url(../images/default/tree/elbow-end.gif); +} + +.x-tree-lines .x-tree-elbow-end-plus{ + background-image:url(../images/default/tree/elbow-end-plus.gif); +} + +.x-tree-lines .x-tree-elbow-end-minus{ + background-image:url(../images/default/tree/elbow-end-minus.gif); +} + +.x-tree-lines .x-tree-elbow-line{ + background-image:url(../images/default/tree/elbow-line.gif); +} + +.x-tree-no-lines .x-tree-elbow-plus{ + background-image:url(../images/default/tree/elbow-plus-nl.gif); +} + +.x-tree-no-lines .x-tree-elbow-minus{ + background-image:url(../images/default/tree/elbow-minus-nl.gif); +} + +.x-tree-no-lines .x-tree-elbow-end-plus{ + background-image:url(../images/default/tree/elbow-end-plus-nl.gif); +} + +.x-tree-no-lines .x-tree-elbow-end-minus{ + background-image:url(../images/default/tree/elbow-end-minus-nl.gif); +} + +.x-tree-arrows .x-tree-elbow-plus{ + background-image:url(../images/default/tree/arrows.gif); +} + +.x-tree-arrows .x-tree-elbow-minus{ + background-image:url(../images/default/tree/arrows.gif); +} + +.x-tree-arrows .x-tree-elbow-end-plus{ + background-image:url(../images/default/tree/arrows.gif); +} + +.x-tree-arrows .x-tree-elbow-end-minus{ + background-image:url(../images/default/tree/arrows.gif); +} + +.x-tree-node{ + color:#000; + font: normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-tree-node a, .x-dd-drag-ghost a{ + color:#000; +} + +.x-tree-node a span, .x-dd-drag-ghost a span{ + color:#000; +} + +.x-tree-node .x-tree-node-disabled a span{ + color:gray !important; +} + +.x-tree-node div.x-tree-drag-insert-below{ + border-bottom-color:#36c; +} + +.x-tree-node div.x-tree-drag-insert-above{ + border-top-color:#36c; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a{ + border-bottom-color:#36c; +} + +.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a{ + border-top-color:#36c; +} + +.x-tree-node .x-tree-drag-append a span{ + background-color:#ddd; + border-color:gray; +} + +.x-tree-node .x-tree-node-over { + background-color: #eee; +} + +.x-tree-node .x-tree-selected { + background-color: #d9e8fb; +} + +.x-tree-drop-ok-append .x-dd-drop-icon{ + background-image: url(../images/default/tree/drop-add.gif); +} + +.x-tree-drop-ok-above .x-dd-drop-icon{ + background-image: url(../images/default/tree/drop-over.gif); +} + +.x-tree-drop-ok-below .x-dd-drop-icon{ + background-image: url(../images/default/tree/drop-under.gif); +} + +.x-tree-drop-ok-between .x-dd-drop-icon{ + background-image: url(../images/default/tree/drop-between.gif); +}.x-date-picker { + border-color: #1b376c; + background-color:#fff; +} + +.x-date-middle,.x-date-left,.x-date-right { + background-image: url(../images/default/shared/hd-sprite.gif); + color:#fff; + font:bold 11px "sans serif", tahoma, verdana, helvetica; +} + +.x-date-middle .x-btn .x-btn-text { + color:#fff; +} + +.x-date-middle .x-btn-mc em.x-btn-arrow { + background-image:url(../images/default/toolbar/btn-arrow-light.gif); +} + +.x-date-right a { + background-image: url(../images/default/shared/right-btn.gif); +} + +.x-date-left a{ + background-image: url(../images/default/shared/left-btn.gif); +} + +.x-date-inner th { + background-color:#dfecfb; + background-image:url(../images/default/shared/glass-bg.gif); + border-bottom-color:#a3bad9; + font:normal 10px arial, helvetica,tahoma,sans-serif; + color:#233d6d; +} + +.x-date-inner td { + border-color:#fff; +} + +.x-date-inner a { + font:normal 11px arial, helvetica,tahoma,sans-serif; + color:#000; +} + +.x-date-inner .x-date-active{ + color:#000; +} + +.x-date-inner .x-date-selected a{ + background-color:#dfecfb; + background-image:url(../images/default/shared/glass-bg.gif); + border-color:#8db2e3; +} + +.x-date-inner .x-date-today a{ + border-color:darkred; +} + +.x-date-inner .x-date-selected span{ + font-weight:bold; +} + +.x-date-inner .x-date-prevday a,.x-date-inner .x-date-nextday a { + color:#aaa; +} + +.x-date-bottom { + border-top-color:#a3bad9; + background-color:#dfecfb; + background-image:url(../images/default/shared/glass-bg.gif); +} + +.x-date-inner a:hover, .x-date-inner .x-date-disabled a:hover{ + color:#000; + background-color:#ddecfe; +} + +.x-date-inner .x-date-disabled a { + background-color:#eee; + color:#bbb; +} + +.x-date-mmenu{ + background-color:#eee !important; +} + +.x-date-mmenu .x-menu-item { + font-size:10px; + color:#000; +} + +.x-date-mp { + background-color:#fff; +} + +.x-date-mp td { + font:normal 11px arial, helvetica,tahoma,sans-serif; +} + +.x-date-mp-btns button { + background-color:#083772; + color:#fff; + border-color: #3366cc #000055 #000055 #3366cc; + font:normal 11px arial, helvetica,tahoma,sans-serif; +} + +.x-date-mp-btns { + background-color: #dfecfb; + background-image: url(../images/default/shared/glass-bg.gif); +} + +.x-date-mp-btns td { + border-top-color: #c5d2df; +} + +td.x-date-mp-month a,td.x-date-mp-year a { + color:#15428b; +} + +td.x-date-mp-month a:hover,td.x-date-mp-year a:hover { + color:#15428b; + background-color: #ddecfe; +} + +td.x-date-mp-sel a { + background-color: #dfecfb; + background-image: url(../images/default/shared/glass-bg.gif); + border-color:#8db2e3; +} + +.x-date-mp-ybtn a { + background-image:url(../images/default/panel/tool-sprites.gif); +} + +td.x-date-mp-sep { + border-right-color:#c5d2df; +}.x-tip .x-tip-close{ + background-image: url(../images/default/qtip/close.gif); +} + +.x-tip .x-tip-tc, .x-tip .x-tip-tl, .x-tip .x-tip-tr, .x-tip .x-tip-bc, .x-tip .x-tip-bl, .x-tip .x-tip-br, .x-tip .x-tip-ml, .x-tip .x-tip-mr { + background-image: url(../images/default/qtip/tip-sprite.gif); +} + +.x-tip .x-tip-mc { + font: normal 11px tahoma,arial,helvetica,sans-serif; +} +.x-tip .x-tip-ml { + background-color: #fff; +} + +.x-tip .x-tip-header-text { + font: bold 11px tahoma,arial,helvetica,sans-serif; + color:#444; +} + +.x-tip .x-tip-body { + font: normal 11px tahoma,arial,helvetica,sans-serif; + color:#444; +} + +.x-form-invalid-tip .x-tip-tc, .x-form-invalid-tip .x-tip-tl, .x-form-invalid-tip .x-tip-tr, .x-form-invalid-tip .x-tip-bc, +.x-form-invalid-tip .x-tip-bl, .x-form-invalid-tip .x-tip-br, .x-form-invalid-tip .x-tip-ml, .x-form-invalid-tip .x-tip-mr +{ + background-image: url(../images/default/form/error-tip-corners.gif); +} + +.x-form-invalid-tip .x-tip-body { + background-image:url(../images/default/form/exclamation.gif); +} + +.x-tip-anchor { + background-image:url(../images/default/qtip/tip-anchor-sprite.gif); +}.x-menu { + background-color:#f0f0f0; + background-image:url(../images/default/menu/menu.gif); +} + +.x-menu-floating{ + border-color:#718bb7; +} + +.x-menu-nosep { + background-image:none; +} + +.x-menu-list-item{ + font:normal 11px arial,tahoma,sans-serif; +} + +.x-menu-item-arrow{ + background-image:url(../images/default/menu/menu-parent.gif); +} + +.x-menu-sep { + background-color:#e0e0e0; + border-bottom-color:#fff; +} + +a.x-menu-item { + color:#222; +} + +.x-menu-item-active { + background-image: url(../images/default/menu/item-over.gif); + background-color: #dbecf4; + border-color:#aaccf6; +} + +.x-menu-item-active a.x-menu-item { + border-color:#aaccf6; +} + +.x-menu-check-item .x-menu-item-icon{ + background-image:url(../images/default/menu/unchecked.gif); +} + +.x-menu-item-checked .x-menu-item-icon{ + background-image:url(../images/default/menu/checked.gif); +} + +.x-menu-item-checked .x-menu-group-item .x-menu-item-icon{ + background-image:url(../images/default/menu/group-checked.gif); +} + +.x-menu-group-item .x-menu-item-icon{ + background-image:none; +} + +.x-menu-plain { + background-color:#f0f0f0 !important; + background-image: none; +} + +.x-date-menu, .x-color-menu{ + background-color: #fff !important; +} + +.x-menu .x-date-picker{ + border-color:#a3bad9; +} + +.x-cycle-menu .x-menu-item-checked { + border-color:#a3bae9 !important; + background-color:#def8f6; +} + +.x-menu-scroller-top { + background-image:url(../images/default/layout/mini-top.gif); +} + +.x-menu-scroller-bottom { + background-image:url(../images/default/layout/mini-bottom.gif); +} +.x-box-tl { + background-image: url(../images/default/box/corners.gif); +} + +.x-box-tc { + background-image: url(../images/default/box/tb.gif); +} + +.x-box-tr { + background-image: url(../images/default/box/corners.gif); +} + +.x-box-ml { + background-image: url(../images/default/box/l.gif); +} + +.x-box-mc { + background-color: #eee; + background-image: url(../images/default/box/tb.gif); + font-family: "Myriad Pro","Myriad Web","Tahoma","Helvetica","Arial",sans-serif; + color: #393939; + font-size: 12px; +} + +.x-box-mc h3 { + font-size: 14px; + font-weight: bold; +} + +.x-box-mr { + background-image: url(../images/default/box/r.gif); +} + +.x-box-bl { + background-image: url(../images/default/box/corners.gif); +} + +.x-box-bc { + background-image: url(../images/default/box/tb.gif); +} + +.x-box-br { + background-image: url(../images/default/box/corners.gif); +} + +.x-box-blue .x-box-bl, .x-box-blue .x-box-br, .x-box-blue .x-box-tl, .x-box-blue .x-box-tr { + background-image: url(../images/default/box/corners-blue.gif); +} + +.x-box-blue .x-box-bc, .x-box-blue .x-box-mc, .x-box-blue .x-box-tc { + background-image: url(../images/default/box/tb-blue.gif); +} + +.x-box-blue .x-box-mc { + background-color: #c3daf9; +} + +.x-box-blue .x-box-mc h3 { + color: #17385b; +} + +.x-box-blue .x-box-ml { + background-image: url(../images/default/box/l-blue.gif); +} + +.x-box-blue .x-box-mr { + background-image: url(../images/default/box/r-blue.gif); +}.x-combo-list { + border-color:#98c0f4; + background-color:#ddecfe; + font:normal 12px tahoma, arial, helvetica, sans-serif; +} + +.x-combo-list-inner { + background-color:#fff; +} + +.x-combo-list-hd { + font:bold 11px tahoma, arial, helvetica, sans-serif; + color:#15428b; + background-image: url(../images/default/layout/panel-title-light-bg.gif); + border-bottom-color:#98c0f4; +} + +.x-resizable-pinned .x-combo-list-inner { + border-bottom-color:#98c0f4; +} + +.x-combo-list-item { + border-color:#fff; +} + +.x-combo-list .x-combo-selected{ + border-color:#a3bae9 !important; + background-color:#dfe8f6; +} + +.x-combo-list .x-toolbar { + border-top-color:#98c0f4; +} + +.x-combo-list-small { + font:normal 11px tahoma, arial, helvetica, sans-serif; +}.x-panel { + border-color: #99bbe8; +} + +.x-panel-header { + color:#15428b; + font-weight:bold; + font-size: 11px; + font-family: tahoma,arial,verdana,sans-serif; + border-color:#99bbe8; + background-image: url(../images/default/panel/white-top-bottom.gif); +} + +.x-panel-body { + border-color:#99bbe8; + background-color:#fff; +} + +.x-panel-bbar .x-toolbar, .x-panel-tbar .x-toolbar { + border-color:#99bbe8; +} + +.x-panel-tbar-noheader .x-toolbar, .x-panel-mc .x-panel-tbar .x-toolbar { + border-top-color:#99bbe8; +} + +.x-panel-body-noheader, .x-panel-mc .x-panel-body { + border-top-color:#99bbe8; +} + +.x-panel-tl .x-panel-header { + color:#15428b; + font:bold 11px tahoma,arial,verdana,sans-serif; +} + +.x-panel-tc { + background-image: url(../images/default/panel/top-bottom.gif); +} + +.x-panel-tl, .x-panel-tr, .x-panel-bl, .x-panel-br{ + background-image: url(../images/default/panel/corners-sprite.gif); + border-bottom-color:#99bbe8; +} + +.x-panel-bc { + background-image: url(../images/default/panel/top-bottom.gif); +} + +.x-panel-mc { + font: normal 11px tahoma,arial,helvetica,sans-serif; + background-color:#dfe8f6; +} + +.x-panel-ml { + background-color: #fff; + background-image:url(../images/default/panel/left-right.gif); +} + +.x-panel-mr { + background-image: url(../images/default/panel/left-right.gif); +} + +.x-tool { + background-image:url(../images/default/panel/tool-sprites.gif); +} + +.x-panel-ghost { + background-color:#cbddf3; +} + +.x-panel-ghost ul { + border-color:#99bbe8; +} + +.x-panel-dd-spacer { + border-color:#99bbe8; +} + +.x-panel-fbar td,.x-panel-fbar span,.x-panel-fbar input,.x-panel-fbar div,.x-panel-fbar select,.x-panel-fbar label{ + font:normal 11px arial,tahoma, helvetica, sans-serif; +} +.x-window-proxy { + background-color:#c7dffc; + border-color:#99bbe8; +} + +.x-window-tl .x-window-header { + color:#15428b; + font:bold 11px tahoma,arial,verdana,sans-serif; +} + +.x-window-tc { + background-image: url(../images/default/window/top-bottom.png); +} + +.x-window-tl { + background-image: url(../images/default/window/left-corners.png); +} + +.x-window-tr { + background-image: url(../images/default/window/right-corners.png); +} + +.x-window-bc { + background-image: url(../images/default/window/top-bottom.png); +} + +.x-window-bl { + background-image: url(../images/default/window/left-corners.png); +} + +.x-window-br { + background-image: url(../images/default/window/right-corners.png); +} + +.x-window-mc { + border-color:#99bbe8; + font: normal 11px tahoma,arial,helvetica,sans-serif; + background-color:#dfe8f6; +} + +.x-window-ml { + background-image: url(../images/default/window/left-right.png); +} + +.x-window-mr { + background-image: url(../images/default/window/left-right.png); +} + +.x-window-maximized .x-window-tc { + background-color:#fff; +} + +.x-window-bbar .x-toolbar { + border-top-color:#99bbe8; +} + +.x-panel-ghost .x-window-tl { + border-bottom-color:#99bbe8; +} + +.x-panel-collapsed .x-window-tl { + border-bottom-color:#84a0c4; +} + +.x-dlg-mask{ + background-color:#ccc; +} + +.x-window-plain .x-window-mc { + background-color: #ccd9e8; + border-color: #a3bae9 #dfe8f6 #dfe8f6 #a3bae9; +} + +.x-window-plain .x-window-body { + border-color: #dfe8f6 #a3bae9 #a3bae9 #dfe8f6; +} + +body.x-body-masked .x-window-plain .x-window-mc { + background-color: #ccd9e8; +}.x-html-editor-wrap { + border-color:#a9bfd3; + background-color:#fff; +} +.x-html-editor-tb .x-btn-text { + background-image:url(../images/default/editor/tb-sprite.gif); +}.x-panel-noborder .x-panel-header-noborder { + border-bottom-color:#99bbe8; +} + +.x-panel-noborder .x-panel-tbar-noborder .x-toolbar { + border-bottom-color:#99bbe8; +} + +.x-panel-noborder .x-panel-bbar-noborder .x-toolbar { + border-top-color:#99bbe8; +} + +.x-tab-panel-bbar-noborder .x-toolbar { + border-top-color:#99bbe8; +} + +.x-tab-panel-tbar-noborder .x-toolbar { + border-bottom-color:#99bbe8; +}.x-border-layout-ct { + background-color:#dfe8f6; +} + +.x-accordion-hd { + color:#222; + font-weight:normal; + background-image: url(../images/default/panel/light-hd.gif); +} + +.x-layout-collapsed{ + background-color:#d2e0f2; + border-color:#98c0f4; +} + +.x-layout-collapsed-over{ + background-color:#d9e8fb; +} + +.x-layout-split-west .x-layout-mini { + background-image:url(../images/default/layout/mini-left.gif); +} +.x-layout-split-east .x-layout-mini { + background-image:url(../images/default/layout/mini-right.gif); +} +.x-layout-split-north .x-layout-mini { + background-image:url(../images/default/layout/mini-top.gif); +} +.x-layout-split-south .x-layout-mini { + background-image:url(../images/default/layout/mini-bottom.gif); +} + +.x-layout-cmini-west .x-layout-mini { + background-image:url(../images/default/layout/mini-right.gif); +} + +.x-layout-cmini-east .x-layout-mini { + background-image:url(../images/default/layout/mini-left.gif); +} + +.x-layout-cmini-north .x-layout-mini { + background-image:url(../images/default/layout/mini-bottom.gif); +} + +.x-layout-cmini-south .x-layout-mini { + background-image:url(../images/default/layout/mini-top.gif); +}.x-progress-wrap { + border-color:#6593cf; +} + +.x-progress-inner { + background-color:#e0e8f3; + background-image:url(../images/default/qtip/bg.gif); +} + +.x-progress-bar { + background-color:#9cbfee; + background-image:url(../images/default/progress/progress-bg.gif); + border-top-color:#d1e4fd; + border-bottom-color:#7fa9e4; + border-right-color:#7fa9e4; +} + +.x-progress-text { + font-size:11px; + font-weight:bold; + color:#fff; +} + +.x-progress-text-back { + color:#396095; +}.x-list-header{ + background-color:#f9f9f9; + background-image:url(../images/default/grid/grid3-hrow.gif); +} + +.x-list-header-inner div em { + border-left-color:#ddd; + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-list-body dt em { + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-list-over { + background-color:#eee; +} + +.x-list-selected { + background-color:#dfe8f6; +} + +.x-list-resizer { + border-left-color:#555; + border-right-color:#555; +} + +.x-list-header-inner em.sort-asc, .x-list-header-inner em.sort-desc { + background-image:url(../images/default/grid/sort-hd.gif); + border-color: #99bbe8; +}.x-slider-horz, .x-slider-horz .x-slider-end, .x-slider-horz .x-slider-inner { + background-image:url(../images/default/slider/slider-bg.png); +} + +.x-slider-horz .x-slider-thumb { + background-image:url(../images/default/slider/slider-thumb.png); +} + +.x-slider-vert, .x-slider-vert .x-slider-end, .x-slider-vert .x-slider-inner { + background-image:url(../images/default/slider/slider-v-bg.png); +} + +.x-slider-vert .x-slider-thumb { + background-image:url(../images/default/slider/slider-v-thumb.png); +}.x-window-dlg .ext-mb-text, +.x-window-dlg .x-window-header-text { + font-size:12px; +} + +.x-window-dlg .ext-mb-textarea { + font:normal 12px tahoma,arial,helvetica,sans-serif; +} + +.x-window-dlg .x-msg-box-wait { + background-image:url(../images/default/grid/loading.gif); +} + +.x-window-dlg .ext-mb-info { + background-image:url(../images/default/window/icon-info.gif); +} + +.x-window-dlg .ext-mb-warning { + background-image:url(../images/default/window/icon-warning.gif); +} + +.x-window-dlg .ext-mb-question { + background-image:url(../images/default/window/icon-question.gif); +} + +.x-window-dlg .ext-mb-error { + background-image:url(../images/default/window/icon-error.gif); +}
\ No newline at end of file diff --git a/modules/organize/vendor/ext/css/ux-all.css b/modules/organize/vendor/ext/css/ux-all.css new file mode 100644 index 0000000..42943be --- /dev/null +++ b/modules/organize/vendor/ext/css/ux-all.css @@ -0,0 +1,772 @@ +/*! + * Ext JS Library 3.3.1 + * Copyright(c) 2006-2010 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +.ux-layout-center-item { + margin:0 auto; + text-align:left; +} +.ux-layout-center .x-panel-body, +body.ux-layout-center { + text-align:center; +} +td.ux-grid-hd-group-cell { + background: url(../../../resources/images/default/grid/grid3-hrow.gif) repeat-x bottom; +}.x-column-tree .x-panel-header { + padding: 3px 0px 0px 0px; + border-bottom-width: 0px; +} + +.x-column-tree .x-panel-header .x-panel-header-text { + margin-left: 3px +} + +.x-column-tree .x-tree-node { + zoom:1; +} +.x-column-tree .x-tree-node-el { + /*border-bottom:1px solid #eee; borders? */ + zoom:1; +} +.x-column-tree .x-tree-selected { + background: #d9e8fb; +} +.x-column-tree .x-tree-node a { + line-height:18px; + vertical-align:middle; +} +.x-column-tree .x-tree-node a span{ + +} +.x-column-tree .x-tree-node .x-tree-selected a span{ + background:transparent; + color:#000; +} +.x-tree-col { + float:left; + overflow:hidden; + padding:0 1px; + zoom:1; +} + +.x-tree-col-text, .x-tree-hd-text { + color:#000; + overflow:hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + padding:3px 3px 3px 5px; + white-space: nowrap; + font:normal 11px arial, tahoma, helvetica, sans-serif; +} + +.x-tree-headers { + margin-top: 3px; + background: #f9f9f9 url(../../../resources/images/default/grid/grid3-hrow.gif) repeat-x 0 bottom; + cursor:default; + zoom:1; +} + +.x-tree-hd { + float:left; + overflow:hidden; + border-left:1px solid #eee; + border-right:1px solid #d0d0d0; +} +/* + * FileUploadField component styles + */ +.x-form-file-wrap { + position: relative; + height: 22px; +} +.x-form-file-wrap .x-form-file { + position: absolute; + right: 0; + -moz-opacity: 0; + filter:alpha(opacity: 0); + opacity: 0; + z-index: 2; + height: 22px; +} +.x-form-file-wrap .x-form-file-btn { + position: absolute; + right: 0; + z-index: 1; +} +.x-form-file-wrap .x-form-file-text { + position: absolute; + left: 0; + z-index: 3; + color: #777; +}/** + * GridFilters Styles + **/ +/* +.x-grid3-hd-row .ux-filtered-column { + border-left: 1px solid #C7E3B4; + border-right: 1px solid #C7E3B4; +} + +.x-grid3-hd-row .ux-filtered-column .x-grid3-hd-inner { + background-image: url(../images/header_bg.gif); +} + +.ux-filtered-column .x-grid3-hd-btn { + background-image: url(../images/hd-btn.gif); +} +*/ +.x-grid3-hd-row td.ux-filtered-column { + font-style: italic; + font-weight: bold; +} + +.ux-filtered-column.sort-asc .x-grid3-sort-icon { + background-image: url(../images/sort_filtered_asc.gif) !important; +} + +.ux-filtered-column.sort-desc .x-grid3-sort-icon { + background-image: url(../images/sort_filtered_desc.gif) !important; +} + +.ux-gridfilter-text-icon { + background-image: url(../images/find.png) !important; +} + +/* Temporary Patch for Bug ??? */ +.x-menu-list-item-indent .x-menu-item-icon { + position: relative; + top: 3px; + left: 3px; + margin-right: 10px; +} +li.x-menu-list-item-indent { + padding-left:0px; +} +li.x-menu-list-item div { + display: block; +} + +/** + * RangeMenu Styles + **/ +.ux-rangemenu-gt { + background-image: url(../images/greater_than.png) !important; +} + +.ux-rangemenu-lt { + background-image: url(../images/less_than.png) !important; +} + +.ux-rangemenu-eq { + background-image: url(../images/equals.png) !important; +} +.x-grid3-summary-row { + border-left:1px solid #fff; + border-right:1px solid #fff; + color:#333; + background: #f1f2f4; +} +.x-grid3-summary-row .x-grid3-cell-inner { + font-weight:bold; + padding-bottom:4px; +} +.x-grid3-cell-first .x-grid3-cell-inner { + padding-left:16px; +} +.x-grid-hide-summary .x-grid3-summary-row { + display:none; +} +.x-grid3-summary-msg { + padding:4px 16px; + font-weight:bold; +}.x-grouptabs-panel { + background-color: #4E78B1; + border: solid 15px #4E78B1; +} +.x-tab-panel-left .x-grouptabs-panel-header, +.x-tab-panel-right .x-grouptabs-panel-header { + float: left; + border: 0; + background: transparent; +} +.x-tab-panel-right .x-grouptabs-panel-header { + float:right; +} +.x-tab-panel-left .x-grouptabs-bwrap { + float: right; + position: relative; +} +.x-tab-panel-right .x-grouptabs-bwrap { + float: left; + position: relative; +} +.x-tab-panel-left ul.x-grouptabs-strip, +.x-tab-panel-right ul.x-grouptabs-strip { + width: auto; + display: block; +} +.x-tab-panel-left ul.x-grouptabs-strip li, +.x-tab-panel-right ul.x-grouptabs-strip li { + padding: 6px 0 2px 6px; + float: none; + margin: 0; + position: relative; + clear: both; +} +.x-tab-panel-left .x-tab-panel-header ul.x-grouptabs-strip a.x-grouptabs-text, +.x-tab-panel-right .x-tab-panel-header ul.x-grouptabs-strip a.x-grouptabs-text{ + font-size: 13px; + line-height: 18px; + cursor: pointer; +} + +.x-tab-panel-left .x-tab-panel-header ul.x-grouptabs-strip a.x-grouptabs-text{ + padding-left: 18px; +} +.x-tab-panel-right .x-tab-panel-header ul.x-grouptabs-strip a.x-grouptabs-text{ + padding-right: 18px; +} + +.x-tab-panel-left .x-tab-panel-header ul.x-grouptabs-sub a.x-grouptabs-text, +.x-tab-panel-right .x-tab-panel-header ul.x-grouptabs-sub a.x-grouptabs-text{ + font-size: 12px; + padding: 0; +} + +.x-tab-panel-left .x-tab-panel-header ul.x-grouptabs-sub a.x-grouptabs-text{ + margin-left: 4px; +} +.x-tab-panel-right .x-tab-panel-header ul.x-grouptabs-sub a.x-grouptabs-text{ + margin-right: 4px; +} + +.x-grouptabs-panel .x-grouptabs-strip a.x-grouptabs-text{ + overflow: hidden; + white-space: nowrap; + display: block; + color: #DFE8F6; + font-family: tahoma, arial, sans-serif; + font-weight: bold; + text-decoration: none; +} +.x-tab-panel-right .x-grouptabs-strip a.x-grouptabs-text { + text-align: right; +} + +.x-grouptabs-panel .x-grouptabs-strip-active a.x-grouptabs-text { + color: #395B8E; +} + +.x-grouptabs-panel ul.x-grouptabs-sub a.x-grouptabs-text { + font-weight: normal; +} +.x-tab-joint { + position: absolute; + width: 3px; + top: 1px; + background: #fff; + z-index: 8999; +} + +.x-grouptabs-panel .x-grouptabs-panel-body { + border: 1px solid #999; +} + +.x-grouptabs-panel ul.x-grouptabs-strip li { + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + border-left: 1px solid transparent; +} + +.x-grouptabs-panel ul.x-grouptabs-strip li.x-grouptabs-strip-active { + border: 0; + background: #fff; + border-top: 1px solid #999; + border-bottom: 1px solid #999; +} + +.x-tab-panel-left ul.x-grouptabs-strip li.x-grouptabs-strip-active { + border-left: 1px solid #999; +} +.x-tab-panel-right ul.x-grouptabs-strip li.x-grouptabs-strip-active { + border-right: 1px solid #999; +} + +.x-grouptabs-panel li.x-grouptabs-strip-active ul.x-grouptabs-sub li.x-grouptabs-strip-active{ + background-color: #EDEEF0; +} + +.x-grouptabs-panel li.x-grouptabs-strip-active ul.x-grouptabs-sub { + background-color: transparent; +} + +.x-grouptabs-panel li.x-grouptabs-strip-active ul.x-grouptabs-sub li { + border-color: transparent; +} + +/* Tab corners */ +.x-grouptabs-panel .x-grouptabs-corner { + background-image: url('../images/x-grouptabs-corners.gif'); + display: none; + width: 11px; + height: 11px; + position: absolute; + font-size: 1px; + line-height: 6px; + overflow: hidden; + zoom:1; +} +.x-grouptabs-panel .x-grouptabs-strip-active .x-grouptabs-corner { + display: block; +} +.x-grouptabs-panel .x-grouptabs-main.x-grouptabs-strip-active ul.x-grouptabs-sub .x-grouptabs-corner { + display: none; +} + +.x-grouptabs-panel .x-grouptabs-corner-top-left { + background-position: top left; + left: 0; top: 0; +} +.x-grouptabs-panel .x-grouptabs-corner-bottom-left { + background-position: bottom left; + left: 0; bottom: 0; +} +.x-grouptabs-panel .x-grouptabs-corner-top-right { + background-position: top right; + right: 0; top: 0; +} +.x-grouptabs-panel .x-grouptabs-corner-bottom-right { + background-position: bottom right; + right: 0; bottom: 0; +} +.x-grouptabs-panel li.x-grouptabs-strip-active .x-grouptabs-corner-bottom-left{ + bottom: -4px; left: -4px; +} +.x-grouptabs-panel li.x-grouptabs-strip-active .x-grouptabs-corner-bottom-right{ + bottom: -4px; right: -4px; +} +.x-grouptabs-panel li.x-grouptabs-strip-active .x-grouptabs-corner-top-left{ + top: -4px; left: -4px; +} +.x-grouptabs-panel li.x-grouptabs-strip-active .x-grouptabs-corner-top-right{ + top: -4px; right: -4px; +} + +.x-grouptabs-panel ul.x-grouptabs-sub li.x-tab-with-icon a.x-grouptabs-text { + background-repeat: no-repeat; + padding-left: 20px; +} + +/* General tab styling */ +.x-grouptabs-panel .x-grouptabs-expand { + background: transparent url('../images/elbow-plus-nl.gif') no-repeat; + width: 16px; + height: 16px; + position: absolute; + left: 7px; + top: 6px; +} + +.ext-ie6 .x-grouptabs-panel .x-grouptabs-expand, +.ext-border-box .x-grouptabs-panel .x-grouptabs-expand { + left: 0; +} + +.x-grouptabs-expanded .x-grouptabs-expand { + background-image: url('../images/elbow-minus-nl.gif'); +} + +/* GroupTabs sub group styling */ +.x-grouptabs-sub { + display: none; + margin-top: 4px; +} + +.x-grouptabs-expanded .x-grouptabs-sub { + display: block; +} + +.x-grouptabs-panel ul.x-grouptabs-sub li { + height: 18px; + margin: 0 0 2px; + padding: 0; +} + +.x-grouptabs-panel ul.x-grouptabs-sub .x-grouptabs-main-item { + display: none; +} + +.x-tab-with-icon{ + border-style:none !important; +} +.x-grid3-locked, .x-grid3-unlocked { + overflow: hidden; + position: absolute; +} + +.x-grid3-locked { + border-right: 1px solid #99BBE8; +} + +.x-grid3-locked .x-grid3-scroller { + overflow: hidden; +} + +.x-grid3-locked .x-grid3-row { + border-right: 0; +} + +.x-grid3-scroll-spacer { + height: 19px; +} + +.x-grid3-unlocked .x-grid3-header-offset { + padding-left: 0; +} + +.x-grid3-unlocked .x-grid3-row { + border-left: 0; +} +.ux-mselect{ + overflow:auto; + background:white; + position:relative; /* for calculating scroll offsets */ + zoom:1; + overflow:auto; +} +.ux-mselect-item{ + font:normal 12px tahoma, arial, helvetica, sans-serif; + padding:2px; + border:1px solid #fff; + white-space: nowrap; + cursor:pointer; +} +.ux-mselect-selected{ + border:1px dotted #a3bae9 !important; + background:#DFE8F6; + cursor:pointer; +} + +.x-view-drag-insert-above { + border-top:1px dotted #3366cc; +} +.x-view-drag-insert-below { + border-bottom:1px dotted #3366cc; +} +.x-panel-resize { + height:5px; + background:transparent url(../images/panel-handle.gif) no-repeat center bottom; + position:relative; + left:0; + top:2px; + cursor:n-resize; + cursor:row-resize; + /* for IE */ + font-size:1px; + line-height:1px; + overflow:hidden; +}.x-portal .x-panel-dd-spacer { + margin-bottom:10px; +} + +.x-portlet { + margin-bottom:10px; +} + +/* Clean up the look of the portlets */ +.x-portlet .x-panel-ml { + padding-left:2px; +} +.x-portlet .x-panel-mr { + padding-right:2px; +} +.x-portlet .x-panel-bl { + padding-left:2px; +} + +.x-portlet .x-panel-br { + padding-right:2px; +} +.x-portlet .x-panel-body { + background:white; +} +.x-portlet .x-panel-mc { + padding-top:2px; +} +.x-portlet .x-panel-bc .x-panel-footer { + padding-bottom:2px; +} +.x-portlet .x-panel-nofooter .x-panel-bc { + height:2px; +}.ext-ie .x-row-editor .x-form-text { + margin:0 !important; +} +.x-row-editor-header { + height:2px; + overflow:hidden; + background: transparent url(../images/row-editor-bg.gif) repeat-x 0 0; +} +.x-row-editor-footer { + height:2px; + overflow:hidden; + background: transparent url(../images/row-editor-bg.gif) repeat-x 0 -2px; +} +.ext-ie .x-row-editor-footer { + margin-top:-1px; +} + +.x-row-editor-body { + overflow:hidden; + zoom:1; + background: #ebf2fb; + padding-top:2px; +} +.x-row-editor .x-btns { + position:absolute; + top:28px; + left:20px; + padding-left:5px; + background: transparent url(../images/row-editor-btns.gif) no-repeat 0 0; +} +.x-row-editor .x-btns .x-plain-bwrap { + padding-right:5px; + background: transparent url(../images/row-editor-btns.gif) no-repeat right -31px; +} +.x-row-editor .x-btns .x-plain-body { + background: transparent url(../images/row-editor-btns.gif) repeat-x 0 -62px; + height:31px; +} +.x-row-editor .x-btns .x-table-layout-cell { + padding:3px; +} + +/* Fixes for IE6/7 trigger fields */ +.ext-ie6 .x-row-editor .x-form-field-wrap .x-form-trigger, .ext-ie7 .x-row-editor .x-form-field-wrap .x-form-trigger { + top: 1px; +} + +.ext-ie6 .x-row-editor .x-form-field-trigger-wrap, .ext-ie7 .x-row-editor .x-form-field-trigger-wrap { + margin-top: -1px; +} + +.errorTip .x-tip-body ul{ + list-style-type:disc; + margin-left:15px; +} +.x-form-spinner-proxy{ + /*background-color:#ff00cc;*/ +} +.x-form-field-wrap .x-form-spinner-trigger { + background:transparent url('../images/spinner.gif') no-repeat 0 0; +} + +.x-form-field-wrap .x-form-spinner-overup{ + background-position:-17px 0; +} +.x-form-field-wrap .x-form-spinner-clickup{ + background-position:-34px 0; +} +.x-form-field-wrap .x-form-spinner-overdown{ + background-position:-51px 0; +} +.x-form-field-wrap .x-form-spinner-clickdown{ + background-position:-68px 0; +} + + +.x-trigger-wrap-focus .x-form-spinner-trigger{ + background-position:-85px 0; +} +.x-trigger-wrap-focus .x-form-spinner-overup{ + background-position:-102px 0; +} +.x-trigger-wrap-focus .x-form-spinner-clickup{ + background-position:-119px 0; +} +.x-trigger-wrap-focus .x-form-spinner-overdown{ + background-position:-136px 0; +} +.x-trigger-wrap-focus .x-form-spinner-clickdown{ + background-position:-153px 0; +} +.x-trigger-wrap-focus .x-form-trigger{ + border-bottom: 1px solid #7eadd9; +} + +.x-form-field-wrap .x-form-spinner-splitter { + line-height:1px; + font-size:1px; + background:transparent url('../images/spinner-split.gif') no-repeat 0 0; + position:absolute; + cursor: n-resize; +} +.x-trigger-wrap-focus .x-form-spinner-splitter{ + background-position:-14px 0; +} +/* StatusBar - structure */ +.x-statusbar .x-status-text { + cursor: default; +/* + height: 21px; + line-height: 21px; + padding: 0 4px; +*/ +} +.x-statusbar .x-status-busy { + padding-left: 25px !important; + background: transparent no-repeat 3px 2px; +} + +.x-toolbar div.xtb-text + +.x-statusbar .x-status-text-panel { + border-top: 1px solid; + border-right: 1px solid; + border-bottom: 1px solid; + border-left: 1px solid; + padding: 2px 8px 2px 5px; +} + +/* StatusBar word processor example styles */ + +#word-status .x-status-text-panel .spacer { + width: 60px; + font-size:0; + line-height:0; +} +#word-status .x-status-busy { + padding-left: 25px !important; + background: transparent no-repeat 3px 2px; +} +#word-status .x-status-saved { + padding-left: 25px !important; + background: transparent no-repeat 3px 2px; +} + +/* StatusBar form validation example styles */ + +.x-statusbar .x-status-error { + cursor: pointer; + padding-left: 25px !important; + background: transparent no-repeat 3px 2px; +} +.x-statusbar .x-status-valid { + padding-left: 25px !important; + background: transparent no-repeat 3px 2px; +} +.x-status-error-list { + font: 11px tahoma,arial,verdana,sans-serif; + position: absolute; + z-index: 9999; + border-top: 1px solid; + border-right: 1px solid; + border-bottom: 1px solid; + border-left: 1px solid; + padding: 5px 10px; +} +.x-status-error-list li { + cursor: pointer; + list-style: disc; + margin-left: 10px; +} +.x-status-error-list li a { + text-decoration: none; +} +.x-status-error-list li a:hover { + text-decoration: underline; +} + + +/* *********************************************************** */ +/* *********************************************************** */ +/* *********************************************************** */ + + +/* StatusBar - visual */ + +.x-statusbar .x-status-busy { + background-image: url(../images/loading.gif); +} +.x-statusbar .x-status-text-panel { + border-color: #99bbe8 #fff #fff #99bbe8; +} + +/* StatusBar word processor example styles */ + +#word-status .x-status-text { + color: #777; +} +#word-status .x-status-busy { + background-image: url(../images/saving.gif); +} +#word-status .x-status-saved { + background-image: url(../images/saved.png); +} + +/* StatusBar form validation example styles */ + +.x-statusbar .x-status-error { + color: #C33; + background-image: url(../images/exclamation.gif); +} +.x-statusbar .x-status-valid { + background-image: url(../images/accept.png); +} +.x-status-error-list { + border-color: #C33; +} +.x-status-error-list li a { + color: #15428B; +}.x-treegrid-root-table { + border-right: 1px solid; +} + +.x-treegrid-root-node { + overflow: auto; +} + +.x-treegrid-hd-hidden { + visibility: hidden; + border: 0; + width: 0; +} + +.x-treegrid-col { + border-bottom: 1px solid; + height: 20px; + overflow: hidden; + vertical-align: top; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} + +.x-treegrid-text { + padding-left: 4px; + -moz-user-select: none; + -khtml-user-select: none; +} + +.x-treegrid-resizer { + border-left:1px solid; + border-right:1px solid; + position:absolute; + left:0; + top:0; +} + +.x-treegrid-header-inner { + overflow: hidden; +} + +.x-treegrid-root-table, +.x-treegrid-col { + border-color: #ededed; +} + +.x-treegrid-resizer { + border-left-color:#555; + border-right-color:#555; +}
\ No newline at end of file diff --git a/modules/organize/vendor/ext/images/default/box/tb-blue.gif b/modules/organize/vendor/ext/images/default/box/tb-blue.gif Binary files differnew file mode 100644 index 0000000..562fecc --- /dev/null +++ b/modules/organize/vendor/ext/images/default/box/tb-blue.gif diff --git a/modules/organize/vendor/ext/images/default/button/btn.gif b/modules/organize/vendor/ext/images/default/button/btn.gif Binary files differnew file mode 100644 index 0000000..06b404d --- /dev/null +++ b/modules/organize/vendor/ext/images/default/button/btn.gif diff --git a/modules/organize/vendor/ext/images/default/dd/drop-no.gif b/modules/organize/vendor/ext/images/default/dd/drop-no.gif Binary files differnew file mode 100644 index 0000000..08d0833 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/dd/drop-no.gif diff --git a/modules/organize/vendor/ext/images/default/dd/drop-yes.gif b/modules/organize/vendor/ext/images/default/dd/drop-yes.gif Binary files differnew file mode 100644 index 0000000..8aacb30 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/dd/drop-yes.gif diff --git a/modules/organize/vendor/ext/images/default/form/text-bg.gif b/modules/organize/vendor/ext/images/default/form/text-bg.gif Binary files differnew file mode 100644 index 0000000..4179607 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/form/text-bg.gif diff --git a/modules/organize/vendor/ext/images/default/form/trigger.gif b/modules/organize/vendor/ext/images/default/form/trigger.gif Binary files differnew file mode 100644 index 0000000..f6cba37 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/form/trigger.gif diff --git a/modules/organize/vendor/ext/images/default/grid/invalid_line.gif b/modules/organize/vendor/ext/images/default/grid/invalid_line.gif Binary files differnew file mode 100644 index 0000000..fb7e0f3 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/grid/invalid_line.gif diff --git a/modules/organize/vendor/ext/images/default/grid/loading.gif b/modules/organize/vendor/ext/images/default/grid/loading.gif Binary files differnew file mode 100644 index 0000000..e846e1d --- /dev/null +++ b/modules/organize/vendor/ext/images/default/grid/loading.gif diff --git a/modules/organize/vendor/ext/images/default/panel/tool-sprites.gif b/modules/organize/vendor/ext/images/default/panel/tool-sprites.gif Binary files differnew file mode 100644 index 0000000..9a3c5b9 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/panel/tool-sprites.gif diff --git a/modules/organize/vendor/ext/images/default/panel/white-top-bottom.gif b/modules/organize/vendor/ext/images/default/panel/white-top-bottom.gif Binary files differnew file mode 100644 index 0000000..fe7dd1c --- /dev/null +++ b/modules/organize/vendor/ext/images/default/panel/white-top-bottom.gif diff --git a/modules/organize/vendor/ext/images/default/progress/progress-bg.gif b/modules/organize/vendor/ext/images/default/progress/progress-bg.gif Binary files differnew file mode 100644 index 0000000..1c1abeb --- /dev/null +++ b/modules/organize/vendor/ext/images/default/progress/progress-bg.gif diff --git a/modules/organize/vendor/ext/images/default/qtip/bg.gif b/modules/organize/vendor/ext/images/default/qtip/bg.gif Binary files differnew file mode 100644 index 0000000..43488af --- /dev/null +++ b/modules/organize/vendor/ext/images/default/qtip/bg.gif diff --git a/modules/organize/vendor/ext/images/default/s.gif b/modules/organize/vendor/ext/images/default/s.gif Binary files differnew file mode 100644 index 0000000..1d11fa9 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/s.gif diff --git a/modules/organize/vendor/ext/images/default/shadow-c.png b/modules/organize/vendor/ext/images/default/shadow-c.png Binary files differnew file mode 100644 index 0000000..d435f80 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/shadow-c.png diff --git a/modules/organize/vendor/ext/images/default/shadow-lr.png b/modules/organize/vendor/ext/images/default/shadow-lr.png Binary files differnew file mode 100644 index 0000000..bb88b6f --- /dev/null +++ b/modules/organize/vendor/ext/images/default/shadow-lr.png diff --git a/modules/organize/vendor/ext/images/default/shadow.png b/modules/organize/vendor/ext/images/default/shadow.png Binary files differnew file mode 100644 index 0000000..75c0eba --- /dev/null +++ b/modules/organize/vendor/ext/images/default/shadow.png diff --git a/modules/organize/vendor/ext/images/default/toolbar/bg.gif b/modules/organize/vendor/ext/images/default/toolbar/bg.gif Binary files differnew file mode 100644 index 0000000..0b085bf --- /dev/null +++ b/modules/organize/vendor/ext/images/default/toolbar/bg.gif diff --git a/modules/organize/vendor/ext/images/default/tree/arrows.gif b/modules/organize/vendor/ext/images/default/tree/arrows.gif Binary files differnew file mode 100644 index 0000000..2683463 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/arrows.gif diff --git a/modules/organize/vendor/ext/images/default/tree/drop-add.gif b/modules/organize/vendor/ext/images/default/tree/drop-add.gif Binary files differnew file mode 100644 index 0000000..b22cd14 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/drop-add.gif diff --git a/modules/organize/vendor/ext/images/default/tree/drop-between.gif b/modules/organize/vendor/ext/images/default/tree/drop-between.gif Binary files differnew file mode 100644 index 0000000..5c6c09d --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/drop-between.gif diff --git a/modules/organize/vendor/ext/images/default/tree/drop-over.gif b/modules/organize/vendor/ext/images/default/tree/drop-over.gif Binary files differnew file mode 100644 index 0000000..30d1ca7 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/drop-over.gif diff --git a/modules/organize/vendor/ext/images/default/tree/folder-open.gif b/modules/organize/vendor/ext/images/default/tree/folder-open.gif Binary files differnew file mode 100644 index 0000000..56ba737 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/folder-open.gif diff --git a/modules/organize/vendor/ext/images/default/tree/folder.gif b/modules/organize/vendor/ext/images/default/tree/folder.gif Binary files differnew file mode 100644 index 0000000..20412f7 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/folder.gif diff --git a/modules/organize/vendor/ext/images/default/tree/loading.gif b/modules/organize/vendor/ext/images/default/tree/loading.gif Binary files differnew file mode 100644 index 0000000..e846e1d --- /dev/null +++ b/modules/organize/vendor/ext/images/default/tree/loading.gif diff --git a/modules/organize/vendor/ext/images/default/window/left-corners.png b/modules/organize/vendor/ext/images/default/window/left-corners.png Binary files differnew file mode 100644 index 0000000..1a51833 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/window/left-corners.png diff --git a/modules/organize/vendor/ext/images/default/window/left-right.png b/modules/organize/vendor/ext/images/default/window/left-right.png Binary files differnew file mode 100644 index 0000000..7586ff3 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/window/left-right.png diff --git a/modules/organize/vendor/ext/images/default/window/right-corners.png b/modules/organize/vendor/ext/images/default/window/right-corners.png Binary files differnew file mode 100644 index 0000000..e69a3ff --- /dev/null +++ b/modules/organize/vendor/ext/images/default/window/right-corners.png diff --git a/modules/organize/vendor/ext/images/default/window/top-bottom.png b/modules/organize/vendor/ext/images/default/window/top-bottom.png Binary files differnew file mode 100644 index 0000000..33779e7 --- /dev/null +++ b/modules/organize/vendor/ext/images/default/window/top-bottom.png diff --git a/modules/organize/vendor/ext/images/fam/delete.gif b/modules/organize/vendor/ext/images/fam/delete.gif Binary files differnew file mode 100644 index 0000000..5e2a3b1 --- /dev/null +++ b/modules/organize/vendor/ext/images/fam/delete.gif diff --git a/modules/organize/vendor/ext/js/ext-organize-bundle.js b/modules/organize/vendor/ext/js/ext-organize-bundle.js new file mode 100644 index 0000000..bf483bd --- /dev/null +++ b/modules/organize/vendor/ext/js/ext-organize-bundle.js @@ -0,0 +1,2202 @@ + +window.undefined=window.undefined;Ext={version:'3.3.1',versionDetail:{major:3,minor:3,patch:1}};Ext.apply=function(o,c,defaults){if(defaults){Ext.apply(o,defaults);} +if(o&&c&&typeof c=='object'){for(var p in c){o[p]=c[p];}} +return o;};(function(){var idSeed=0,toString=Object.prototype.toString,ua=navigator.userAgent.toLowerCase(),check=function(r){return r.test(ua);},DOC=document,docMode=DOC.documentMode,isStrict=DOC.compatMode=="CSS1Compat",isOpera=check(/opera/),isChrome=check(/\bchrome\b/),isWebKit=check(/webkit/),isSafari=!isChrome&&check(/safari/),isSafari2=isSafari&&check(/applewebkit\/4/),isSafari3=isSafari&&check(/version\/3/),isSafari4=isSafari&&check(/version\/4/),isIE=!isOpera&&check(/msie/),isIE7=isIE&&(check(/msie 7/)||docMode==7),isIE8=isIE&&(check(/msie 8/)&&docMode!=7),isIE6=isIE&&!isIE7&&!isIE8,isGecko=!isWebKit&&check(/gecko/),isGecko2=isGecko&&check(/rv:1\.8/),isGecko3=isGecko&&check(/rv:1\.9/),isBorderBox=isIE&&!isStrict,isWindows=check(/windows|win32/),isMac=check(/macintosh|mac os x/),isAir=check(/adobeair/),isLinux=check(/linux/),isSecure=/^https/i.test(window.location.protocol);if(isIE6){try{DOC.execCommand("BackgroundImageCache",false,true);}catch(e){}} +Ext.apply(Ext,{SSL_SECURE_URL:isSecure&&isIE?'javascript:""':'about:blank',isStrict:isStrict,isSecure:isSecure,isReady:false,enableForcedBoxModel:false,enableGarbageCollector:true,enableListenerCollection:false,enableNestedListenerRemoval:false,USE_NATIVE_JSON:false,applyIf:function(o,c){if(o){for(var p in c){if(!Ext.isDefined(o[p])){o[p]=c[p];}}} +return o;},id:function(el,prefix){el=Ext.getDom(el,true)||{};if(!el.id){el.id=(prefix||"ext-gen")+(++idSeed);} +return el.id;},extend:function(){var io=function(o){for(var m in o){this[m]=o[m];}};var oc=Object.prototype.constructor;return function(sb,sp,overrides){if(typeof sp=='object'){overrides=sp;sp=sb;sb=overrides.constructor!=oc?overrides.constructor:function(){sp.apply(this,arguments);};} +var F=function(){},sbp,spp=sp.prototype;F.prototype=spp;sbp=sb.prototype=new F();sbp.constructor=sb;sb.superclass=spp;if(spp.constructor==oc){spp.constructor=sp;} +sb.override=function(o){Ext.override(sb,o);};sbp.superclass=sbp.supr=(function(){return spp;});sbp.override=io;Ext.override(sb,overrides);sb.extend=function(o){return Ext.extend(sb,o);};return sb;};}(),override:function(origclass,overrides){if(overrides){var p=origclass.prototype;Ext.apply(p,overrides);if(Ext.isIE&&overrides.hasOwnProperty('toString')){p.toString=overrides.toString;}}},namespace:function(){var o,d;Ext.each(arguments,function(v){d=v.split(".");o=window[d[0]]=window[d[0]]||{};Ext.each(d.slice(1),function(v2){o=o[v2]=o[v2]||{};});});return o;},urlEncode:function(o,pre){var empty,buf=[],e=encodeURIComponent;Ext.iterate(o,function(key,item){empty=Ext.isEmpty(item);Ext.each(empty?key:item,function(val){buf.push('&',e(key),'=',(!Ext.isEmpty(val)&&(val!=key||!empty))?(Ext.isDate(val)?Ext.encode(val).replace(/"/g,''):e(val)):'');});});if(!pre){buf.shift();pre='';} +return pre+buf.join('');},urlDecode:function(string,overwrite){if(Ext.isEmpty(string)){return{};} +var obj={},pairs=string.split('&'),d=decodeURIComponent,name,value;Ext.each(pairs,function(pair){pair=pair.split('=');name=d(pair[0]);value=d(pair[1]);obj[name]=overwrite||!obj[name]?value:[].concat(obj[name]).concat(value);});return obj;},urlAppend:function(url,s){if(!Ext.isEmpty(s)){return url+(url.indexOf('?')===-1?'?':'&')+s;} +return url;},toArray:function(){return isIE?function(a,i,j,res){res=[];for(var x=0,len=a.length;x<len;x++){res.push(a[x]);} +return res.slice(i||0,j||res.length);}:function(a,i,j){return Array.prototype.slice.call(a,i||0,j||a.length);};}(),isIterable:function(v){if(Ext.isArray(v)||v.callee){return true;} +if(/NodeList|HTMLCollection/.test(toString.call(v))){return true;} +return((typeof v.nextNode!='undefined'||v.item)&&Ext.isNumber(v.length));},each:function(array,fn,scope){if(Ext.isEmpty(array,true)){return;} +if(!Ext.isIterable(array)||Ext.isPrimitive(array)){array=[array];} +for(var i=0,len=array.length;i<len;i++){if(fn.call(scope||array[i],array[i],i,array)===false){return i;};}},iterate:function(obj,fn,scope){if(Ext.isEmpty(obj)){return;} +if(Ext.isIterable(obj)){Ext.each(obj,fn,scope);return;}else if(typeof obj=='object'){for(var prop in obj){if(obj.hasOwnProperty(prop)){if(fn.call(scope||obj,prop,obj[prop],obj)===false){return;};}}}},getDom:function(el,strict){if(!el||!DOC){return null;} +if(el.dom){return el.dom;}else{if(typeof el=='string'){var e=DOC.getElementById(el);if(e&&isIE&&strict){if(el==e.getAttribute('id')){return e;}else{return null;}} +return e;}else{return el;}}},getBody:function(){return Ext.get(DOC.body||DOC.documentElement);},getHead:function(){var head;return function(){if(head==undefined){head=Ext.get(DOC.getElementsByTagName("head")[0]);} +return head;};}(),removeNode:isIE&&!isIE8?function(){var d;return function(n){if(n&&n.tagName!='BODY'){(Ext.enableNestedListenerRemoval)?Ext.EventManager.purgeElement(n,true):Ext.EventManager.removeAll(n);d=d||DOC.createElement('div');d.appendChild(n);d.innerHTML='';delete Ext.elCache[n.id];}};}():function(n){if(n&&n.parentNode&&n.tagName!='BODY'){(Ext.enableNestedListenerRemoval)?Ext.EventManager.purgeElement(n,true):Ext.EventManager.removeAll(n);n.parentNode.removeChild(n);delete Ext.elCache[n.id];}},isEmpty:function(v,allowBlank){return v===null||v===undefined||((Ext.isArray(v)&&!v.length))||(!allowBlank?v==='':false);},isArray:function(v){return toString.apply(v)==='[object Array]';},isDate:function(v){return toString.apply(v)==='[object Date]';},isObject:function(v){return!!v&&Object.prototype.toString.call(v)==='[object Object]';},isPrimitive:function(v){return Ext.isString(v)||Ext.isNumber(v)||Ext.isBoolean(v);},isFunction:function(v){return toString.apply(v)==='[object Function]';},isNumber:function(v){return typeof v==='number'&&isFinite(v);},isString:function(v){return typeof v==='string';},isBoolean:function(v){return typeof v==='boolean';},isElement:function(v){return v?!!v.tagName:false;},isDefined:function(v){return typeof v!=='undefined';},isOpera:isOpera,isWebKit:isWebKit,isChrome:isChrome,isSafari:isSafari,isSafari3:isSafari3,isSafari4:isSafari4,isSafari2:isSafari2,isIE:isIE,isIE6:isIE6,isIE7:isIE7,isIE8:isIE8,isGecko:isGecko,isGecko2:isGecko2,isGecko3:isGecko3,isBorderBox:isBorderBox,isLinux:isLinux,isWindows:isWindows,isMac:isMac,isAir:isAir});Ext.ns=Ext.namespace;})();Ext.ns('Ext.util','Ext.lib','Ext.data','Ext.supports');Ext.elCache={};Ext.apply(Function.prototype,{createInterceptor:function(fcn,scope){var method=this;return!Ext.isFunction(fcn)?this:function(){var me=this,args=arguments;fcn.target=me;fcn.method=method;return(fcn.apply(scope||me||window,args)!==false)?method.apply(me||window,args):null;};},createCallback:function(){var args=arguments,method=this;return function(){return method.apply(window,args);};},createDelegate:function(obj,args,appendArgs){var method=this;return function(){var callArgs=args||arguments;if(appendArgs===true){callArgs=Array.prototype.slice.call(arguments,0);callArgs=callArgs.concat(args);}else if(Ext.isNumber(appendArgs)){callArgs=Array.prototype.slice.call(arguments,0);var applyArgs=[appendArgs,0].concat(args);Array.prototype.splice.apply(callArgs,applyArgs);} +return method.apply(obj||window,callArgs);};},defer:function(millis,obj,args,appendArgs){var fn=this.createDelegate(obj,args,appendArgs);if(millis>0){return setTimeout(fn,millis);} +fn();return 0;}});Ext.applyIf(String,{format:function(format){var args=Ext.toArray(arguments,1);return format.replace(/\{(\d+)\}/g,function(m,i){return args[i];});}});Ext.applyIf(Array.prototype,{indexOf:function(o,from){var len=this.length;from=from||0;from+=(from<0)?len:0;for(;from<len;++from){if(this[from]===o){return from;}} +return-1;},remove:function(o){var index=this.indexOf(o);if(index!=-1){this.splice(index,1);} +return this;}});Ext.util.TaskRunner=function(interval){interval=interval||10;var tasks=[],removeQueue=[],id=0,running=false,stopThread=function(){running=false;clearInterval(id);id=0;},startThread=function(){if(!running){running=true;id=setInterval(runTasks,interval);}},removeTask=function(t){removeQueue.push(t);if(t.onStop){t.onStop.apply(t.scope||t);}},runTasks=function(){var rqLen=removeQueue.length,now=new Date().getTime();if(rqLen>0){for(var i=0;i<rqLen;i++){tasks.remove(removeQueue[i]);} +removeQueue=[];if(tasks.length<1){stopThread();return;}} +for(var i=0,t,itime,rt,len=tasks.length;i<len;++i){t=tasks[i];itime=now-t.taskRunTime;if(t.interval<=itime){rt=t.run.apply(t.scope||t,t.args||[++t.taskRunCount]);t.taskRunTime=now;if(rt===false||t.taskRunCount===t.repeat){removeTask(t);return;}} +if(t.duration&&t.duration<=(now-t.taskStartTime)){removeTask(t);}}};this.start=function(task){tasks.push(task);task.taskStartTime=new Date().getTime();task.taskRunTime=0;task.taskRunCount=0;startThread();return task;};this.stop=function(task){removeTask(task);return task;};this.stopAll=function(){stopThread();for(var i=0,len=tasks.length;i<len;i++){if(tasks[i].onStop){tasks[i].onStop();}} +tasks=[];removeQueue=[];};};Ext.TaskMgr=new Ext.util.TaskRunner();(function(){var libFlyweight;function fly(el){if(!libFlyweight){libFlyweight=new Ext.Element.Flyweight();} +libFlyweight.dom=el;return libFlyweight;} +(function(){var doc=document,isCSS1=doc.compatMode=="CSS1Compat",MAX=Math.max,ROUND=Math.round,PARSEINT=parseInt;Ext.lib.Dom={isAncestor:function(p,c){var ret=false;p=Ext.getDom(p);c=Ext.getDom(c);if(p&&c){if(p.contains){return p.contains(c);}else if(p.compareDocumentPosition){return!!(p.compareDocumentPosition(c)&16);}else{while(c=c.parentNode){ret=c==p||ret;}}} +return ret;},getViewWidth:function(full){return full?this.getDocumentWidth():this.getViewportWidth();},getViewHeight:function(full){return full?this.getDocumentHeight():this.getViewportHeight();},getDocumentHeight:function(){return MAX(!isCSS1?doc.body.scrollHeight:doc.documentElement.scrollHeight,this.getViewportHeight());},getDocumentWidth:function(){return MAX(!isCSS1?doc.body.scrollWidth:doc.documentElement.scrollWidth,this.getViewportWidth());},getViewportHeight:function(){return Ext.isIE?(Ext.isStrict?doc.documentElement.clientHeight:doc.body.clientHeight):self.innerHeight;},getViewportWidth:function(){return!Ext.isStrict&&!Ext.isOpera?doc.body.clientWidth:Ext.isIE?doc.documentElement.clientWidth:self.innerWidth;},getY:function(el){return this.getXY(el)[1];},getX:function(el){return this.getXY(el)[0];},getXY:function(el){var p,pe,b,bt,bl,dbd,x=0,y=0,scroll,hasAbsolute,bd=(doc.body||doc.documentElement),ret=[0,0];el=Ext.getDom(el);if(el!=bd){if(el.getBoundingClientRect){b=el.getBoundingClientRect();scroll=fly(document).getScroll();ret=[ROUND(b.left+scroll.left),ROUND(b.top+scroll.top)];}else{p=el;hasAbsolute=fly(el).isStyle("position","absolute");while(p){pe=fly(p);x+=p.offsetLeft;y+=p.offsetTop;hasAbsolute=hasAbsolute||pe.isStyle("position","absolute");if(Ext.isGecko){y+=bt=PARSEINT(pe.getStyle("borderTopWidth"),10)||0;x+=bl=PARSEINT(pe.getStyle("borderLeftWidth"),10)||0;if(p!=el&&!pe.isStyle('overflow','visible')){x+=bl;y+=bt;}} +p=p.offsetParent;} +if(Ext.isSafari&&hasAbsolute){x-=bd.offsetLeft;y-=bd.offsetTop;} +if(Ext.isGecko&&!hasAbsolute){dbd=fly(bd);x+=PARSEINT(dbd.getStyle("borderLeftWidth"),10)||0;y+=PARSEINT(dbd.getStyle("borderTopWidth"),10)||0;} +p=el.parentNode;while(p&&p!=bd){if(!Ext.isOpera||(p.tagName!='TR'&&!fly(p).isStyle("display","inline"))){x-=p.scrollLeft;y-=p.scrollTop;} +p=p.parentNode;} +ret=[x,y];}} +return ret;},setXY:function(el,xy){(el=Ext.fly(el,'_setXY')).position();var pts=el.translatePoints(xy),style=el.dom.style,pos;for(pos in pts){if(!isNaN(pts[pos])){style[pos]=pts[pos]+"px";}}},setX:function(el,x){this.setXY(el,[x,false]);},setY:function(el,y){this.setXY(el,[false,y]);}};})();Ext.lib.Event=function(){var loadComplete=false,unloadListeners={},retryCount=0,onAvailStack=[],_interval,locked=false,win=window,doc=document,POLL_RETRYS=200,POLL_INTERVAL=20,TYPE=0,FN=1,OBJ=2,ADJ_SCOPE=3,SCROLLLEFT='scrollLeft',SCROLLTOP='scrollTop',UNLOAD='unload',MOUSEOVER='mouseover',MOUSEOUT='mouseout',doAdd=function(){var ret;if(win.addEventListener){ret=function(el,eventName,fn,capture){if(eventName=='mouseenter'){fn=fn.createInterceptor(checkRelatedTarget);el.addEventListener(MOUSEOVER,fn,(capture));}else if(eventName=='mouseleave'){fn=fn.createInterceptor(checkRelatedTarget);el.addEventListener(MOUSEOUT,fn,(capture));}else{el.addEventListener(eventName,fn,(capture));} +return fn;};}else if(win.attachEvent){ret=function(el,eventName,fn,capture){el.attachEvent("on"+eventName,fn);return fn;};}else{ret=function(){};} +return ret;}(),doRemove=function(){var ret;if(win.removeEventListener){ret=function(el,eventName,fn,capture){if(eventName=='mouseenter'){eventName=MOUSEOVER;}else if(eventName=='mouseleave'){eventName=MOUSEOUT;} +el.removeEventListener(eventName,fn,(capture));};}else if(win.detachEvent){ret=function(el,eventName,fn){el.detachEvent("on"+eventName,fn);};}else{ret=function(){};} +return ret;}();function checkRelatedTarget(e){return!elContains(e.currentTarget,pub.getRelatedTarget(e));} +function elContains(parent,child){if(parent&&parent.firstChild){while(child){if(child===parent){return true;} +child=child.parentNode;if(child&&(child.nodeType!=1)){child=null;}}} +return false;} +function _tryPreloadAttach(){var ret=false,notAvail=[],element,i,v,override,tryAgain=!loadComplete||(retryCount>0);if(!locked){locked=true;for(i=0;i<onAvailStack.length;++i){v=onAvailStack[i];if(v&&(element=doc.getElementById(v.id))){if(!v.checkReady||loadComplete||element.nextSibling||(doc&&doc.body)){override=v.override;element=override?(override===true?v.obj:override):element;v.fn.call(element,v.obj);onAvailStack.remove(v);--i;}else{notAvail.push(v);}}} +retryCount=(notAvail.length===0)?0:retryCount-1;if(tryAgain){startInterval();}else{clearInterval(_interval);_interval=null;} +ret=!(locked=false);} +return ret;} +function startInterval(){if(!_interval){var callback=function(){_tryPreloadAttach();};_interval=setInterval(callback,POLL_INTERVAL);}} +function getScroll(){var dd=doc.documentElement,db=doc.body;if(dd&&(dd[SCROLLTOP]||dd[SCROLLLEFT])){return[dd[SCROLLLEFT],dd[SCROLLTOP]];}else if(db){return[db[SCROLLLEFT],db[SCROLLTOP]];}else{return[0,0];}} +function getPageCoord(ev,xy){ev=ev.browserEvent||ev;var coord=ev['page'+xy];if(!coord&&coord!==0){coord=ev['client'+xy]||0;if(Ext.isIE){coord+=getScroll()[xy=="X"?0:1];}} +return coord;} +var pub={extAdapter:true,onAvailable:function(p_id,p_fn,p_obj,p_override){onAvailStack.push({id:p_id,fn:p_fn,obj:p_obj,override:p_override,checkReady:false});retryCount=POLL_RETRYS;startInterval();},addListener:function(el,eventName,fn){el=Ext.getDom(el);if(el&&fn){if(eventName==UNLOAD){if(unloadListeners[el.id]===undefined){unloadListeners[el.id]=[];} +unloadListeners[el.id].push([eventName,fn]);return fn;} +return doAdd(el,eventName,fn,false);} +return false;},removeListener:function(el,eventName,fn){el=Ext.getDom(el);var i,len,li,lis;if(el&&fn){if(eventName==UNLOAD){if((lis=unloadListeners[el.id])!==undefined){for(i=0,len=lis.length;i<len;i++){if((li=lis[i])&&li[TYPE]==eventName&&li[FN]==fn){unloadListeners[el.id].splice(i,1);}}} +return;} +doRemove(el,eventName,fn,false);}},getTarget:function(ev){ev=ev.browserEvent||ev;return this.resolveTextNode(ev.target||ev.srcElement);},resolveTextNode:Ext.isGecko?function(node){if(!node){return;} +var s=HTMLElement.prototype.toString.call(node);if(s=='[xpconnect wrapped native prototype]'||s=='[object XULElement]'){return;} +return node.nodeType==3?node.parentNode:node;}:function(node){return node&&node.nodeType==3?node.parentNode:node;},getRelatedTarget:function(ev){ev=ev.browserEvent||ev;return this.resolveTextNode(ev.relatedTarget||(/(mouseout|mouseleave)/.test(ev.type)?ev.toElement:/(mouseover|mouseenter)/.test(ev.type)?ev.fromElement:null));},getPageX:function(ev){return getPageCoord(ev,"X");},getPageY:function(ev){return getPageCoord(ev,"Y");},getXY:function(ev){return[this.getPageX(ev),this.getPageY(ev)];},stopEvent:function(ev){this.stopPropagation(ev);this.preventDefault(ev);},stopPropagation:function(ev){ev=ev.browserEvent||ev;if(ev.stopPropagation){ev.stopPropagation();}else{ev.cancelBubble=true;}},preventDefault:function(ev){ev=ev.browserEvent||ev;if(ev.preventDefault){ev.preventDefault();}else{ev.returnValue=false;}},getEvent:function(e){e=e||win.event;if(!e){var c=this.getEvent.caller;while(c){e=c.arguments[0];if(e&&Event==e.constructor){break;} +c=c.caller;}} +return e;},getCharCode:function(ev){ev=ev.browserEvent||ev;return ev.charCode||ev.keyCode||0;},getListeners:function(el,eventName){Ext.EventManager.getListeners(el,eventName);},purgeElement:function(el,recurse,eventName){Ext.EventManager.purgeElement(el,recurse,eventName);},_load:function(e){loadComplete=true;if(Ext.isIE&&e!==true){doRemove(win,"load",arguments.callee);}},_unload:function(e){var EU=Ext.lib.Event,i,v,ul,id,len,scope;for(id in unloadListeners){ul=unloadListeners[id];for(i=0,len=ul.length;i<len;i++){v=ul[i];if(v){try{scope=v[ADJ_SCOPE]?(v[ADJ_SCOPE]===true?v[OBJ]:v[ADJ_SCOPE]):win;v[FN].call(scope,EU.getEvent(e),v[OBJ]);}catch(ex){}}}};Ext.EventManager._unload();doRemove(win,UNLOAD,EU._unload);}};pub.on=pub.addListener;pub.un=pub.removeListener;if(doc&&doc.body){pub._load(true);}else{doAdd(win,"load",pub._load);} +doAdd(win,UNLOAD,pub._unload);_tryPreloadAttach();return pub;}();Ext.lib.Ajax=function(){var activeX=['Msxml2.XMLHTTP.6.0','Msxml2.XMLHTTP.3.0','Msxml2.XMLHTTP'],CONTENTTYPE='Content-Type';function setHeader(o){var conn=o.conn,prop,headers={};function setTheHeaders(conn,headers){for(prop in headers){if(headers.hasOwnProperty(prop)){conn.setRequestHeader(prop,headers[prop]);}}} +Ext.apply(headers,pub.headers,pub.defaultHeaders);setTheHeaders(conn,headers);delete pub.headers;} +function createExceptionObject(tId,callbackArg,isAbort,isTimeout){return{tId:tId,status:isAbort?-1:0,statusText:isAbort?'transaction aborted':'communication failure',isAbort:isAbort,isTimeout:isTimeout,argument:callbackArg};} +function initHeader(label,value){(pub.headers=pub.headers||{})[label]=value;} +function createResponseObject(o,callbackArg){var headerObj={},headerStr,conn=o.conn,t,s,isBrokenStatus=conn.status==1223;try{headerStr=o.conn.getAllResponseHeaders();Ext.each(headerStr.replace(/\r\n/g,'\n').split('\n'),function(v){t=v.indexOf(':');if(t>=0){s=v.substr(0,t).toLowerCase();if(v.charAt(t+1)==' '){++t;} +headerObj[s]=v.substr(t+1);}});}catch(e){} +return{tId:o.tId,status:isBrokenStatus?204:conn.status,statusText:isBrokenStatus?'No Content':conn.statusText,getResponseHeader:function(header){return headerObj[header.toLowerCase()];},getAllResponseHeaders:function(){return headerStr;},responseText:conn.responseText,responseXML:conn.responseXML,argument:callbackArg};} +function releaseObject(o){if(o.tId){pub.conn[o.tId]=null;} +o.conn=null;o=null;} +function handleTransactionResponse(o,callback,isAbort,isTimeout){if(!callback){releaseObject(o);return;} +var httpStatus,responseObject;try{if(o.conn.status!==undefined&&o.conn.status!=0){httpStatus=o.conn.status;} +else{httpStatus=13030;}} +catch(e){httpStatus=13030;} +if((httpStatus>=200&&httpStatus<300)||(Ext.isIE&&httpStatus==1223)){responseObject=createResponseObject(o,callback.argument);if(callback.success){if(!callback.scope){callback.success(responseObject);} +else{callback.success.apply(callback.scope,[responseObject]);}}} +else{switch(httpStatus){case 12002:case 12029:case 12030:case 12031:case 12152:case 13030:responseObject=createExceptionObject(o.tId,callback.argument,(isAbort?isAbort:false),isTimeout);if(callback.failure){if(!callback.scope){callback.failure(responseObject);} +else{callback.failure.apply(callback.scope,[responseObject]);}} +break;default:responseObject=createResponseObject(o,callback.argument);if(callback.failure){if(!callback.scope){callback.failure(responseObject);} +else{callback.failure.apply(callback.scope,[responseObject]);}}}} +releaseObject(o);responseObject=null;} +function checkResponse(o,callback,conn,tId,poll,cbTimeout){if(conn&&conn.readyState==4){clearInterval(poll[tId]);poll[tId]=null;if(cbTimeout){clearTimeout(pub.timeout[tId]);pub.timeout[tId]=null;} +handleTransactionResponse(o,callback);}} +function checkTimeout(o,callback){pub.abort(o,callback,true);} +function handleReadyState(o,callback){callback=callback||{};var conn=o.conn,tId=o.tId,poll=pub.poll,cbTimeout=callback.timeout||null;if(cbTimeout){pub.conn[tId]=conn;pub.timeout[tId]=setTimeout(checkTimeout.createCallback(o,callback),cbTimeout);} +poll[tId]=setInterval(checkResponse.createCallback(o,callback,conn,tId,poll,cbTimeout),pub.pollInterval);} +function asyncRequest(method,uri,callback,postData){var o=getConnectionObject()||null;if(o){o.conn.open(method,uri,true);if(pub.useDefaultXhrHeader){initHeader('X-Requested-With',pub.defaultXhrHeader);} +if(postData&&pub.useDefaultHeader&&(!pub.headers||!pub.headers[CONTENTTYPE])){initHeader(CONTENTTYPE,pub.defaultPostHeader);} +if(pub.defaultHeaders||pub.headers){setHeader(o);} +handleReadyState(o,callback);o.conn.send(postData||null);} +return o;} +function getConnectionObject(){var o;try{if(o=createXhrObject(pub.transactionId)){pub.transactionId++;}}catch(e){}finally{return o;}} +function createXhrObject(transactionId){var http;try{http=new XMLHttpRequest();}catch(e){for(var i=0;i<activeX.length;++i){try{http=new ActiveXObject(activeX[i]);break;}catch(e){}}}finally{return{conn:http,tId:transactionId};}} +var pub={request:function(method,uri,cb,data,options){if(options){var me=this,xmlData=options.xmlData,jsonData=options.jsonData,hs;Ext.applyIf(me,options);if(xmlData||jsonData){hs=me.headers;if(!hs||!hs[CONTENTTYPE]){initHeader(CONTENTTYPE,xmlData?'text/xml':'application/json');} +data=xmlData||(!Ext.isPrimitive(jsonData)?Ext.encode(jsonData):jsonData);}} +return asyncRequest(method||options.method||"POST",uri,cb,data);},serializeForm:function(form){var fElements=form.elements||(document.forms[form]||Ext.getDom(form)).elements,hasSubmit=false,encoder=encodeURIComponent,name,data='',type,hasValue;Ext.each(fElements,function(element){name=element.name;type=element.type;if(!element.disabled&&name){if(/select-(one|multiple)/i.test(type)){Ext.each(element.options,function(opt){if(opt.selected){hasValue=opt.hasAttribute?opt.hasAttribute('value'):opt.getAttributeNode('value').specified;data+=String.format("{0}={1}&",encoder(name),encoder(hasValue?opt.value:opt.text));}});}else if(!(/file|undefined|reset|button/i.test(type))){if(!(/radio|checkbox/i.test(type)&&!element.checked)&&!(type=='submit'&&hasSubmit)){data+=encoder(name)+'='+encoder(element.value)+'&';hasSubmit=/submit/i.test(type);}}}});return data.substr(0,data.length-1);},useDefaultHeader:true,defaultPostHeader:'application/x-www-form-urlencoded; charset=UTF-8',useDefaultXhrHeader:true,defaultXhrHeader:'XMLHttpRequest',poll:{},timeout:{},conn:{},pollInterval:50,transactionId:0,abort:function(o,callback,isTimeout){var me=this,tId=o.tId,isAbort=false;if(me.isCallInProgress(o)){o.conn.abort();clearInterval(me.poll[tId]);me.poll[tId]=null;clearTimeout(pub.timeout[tId]);me.timeout[tId]=null;handleTransactionResponse(o,callback,(isAbort=true),isTimeout);} +return isAbort;},isCallInProgress:function(o){return o.conn&&!{0:true,4:true}[o.conn.readyState];}};return pub;}();(function(){var EXTLIB=Ext.lib,noNegatives=/width|height|opacity|padding/i,offsetAttribute=/^((width|height)|(top|left))$/,defaultUnit=/width|height|top$|bottom$|left$|right$/i,offsetUnit=/\d+(em|%|en|ex|pt|in|cm|mm|pc)$/i,isset=function(v){return typeof v!=='undefined';},now=function(){return new Date();};EXTLIB.Anim={motion:function(el,args,duration,easing,cb,scope){return this.run(el,args,duration,easing,cb,scope,Ext.lib.Motion);},run:function(el,args,duration,easing,cb,scope,type){type=type||Ext.lib.AnimBase;if(typeof easing=="string"){easing=Ext.lib.Easing[easing];} +var anim=new type(el,args,duration,easing);anim.animateX(function(){if(Ext.isFunction(cb)){cb.call(scope);}});return anim;}};EXTLIB.AnimBase=function(el,attributes,duration,method){if(el){this.init(el,attributes,duration,method);}};EXTLIB.AnimBase.prototype={doMethod:function(attr,start,end){var me=this;return me.method(me.curFrame,start,end-start,me.totalFrames);},setAttr:function(attr,val,unit){if(noNegatives.test(attr)&&val<0){val=0;} +Ext.fly(this.el,'_anim').setStyle(attr,val+unit);},getAttr:function(attr){var el=Ext.fly(this.el),val=el.getStyle(attr),a=offsetAttribute.exec(attr)||[];if(val!=='auto'&&!offsetUnit.test(val)){return parseFloat(val);} +return(!!(a[2])||(el.getStyle('position')=='absolute'&&!!(a[3])))?el.dom['offset'+a[0].charAt(0).toUpperCase()+a[0].substr(1)]:0;},getDefaultUnit:function(attr){return defaultUnit.test(attr)?'px':'';},animateX:function(callback,scope){var me=this,f=function(){me.onComplete.removeListener(f);if(Ext.isFunction(callback)){callback.call(scope||me,me);}};me.onComplete.addListener(f,me);me.animate();},setRunAttr:function(attr){var me=this,a=this.attributes[attr],to=a.to,by=a.by,from=a.from,unit=a.unit,ra=(this.runAttrs[attr]={}),end;if(!isset(to)&&!isset(by)){return false;} +var start=isset(from)?from:me.getAttr(attr);if(isset(to)){end=to;}else if(isset(by)){if(Ext.isArray(start)){end=[];for(var i=0,len=start.length;i<len;i++){end[i]=start[i]+by[i];}}else{end=start+by;}} +Ext.apply(ra,{start:start,end:end,unit:isset(unit)?unit:me.getDefaultUnit(attr)});},init:function(el,attributes,duration,method){var me=this,actualFrames=0,mgr=EXTLIB.AnimMgr;Ext.apply(me,{isAnimated:false,startTime:null,el:Ext.getDom(el),attributes:attributes||{},duration:duration||1,method:method||EXTLIB.Easing.easeNone,useSec:true,curFrame:0,totalFrames:mgr.fps,runAttrs:{},animate:function(){var me=this,d=me.duration;if(me.isAnimated){return false;} +me.curFrame=0;me.totalFrames=me.useSec?Math.ceil(mgr.fps*d):d;mgr.registerElement(me);},stop:function(finish){var me=this;if(finish){me.curFrame=me.totalFrames;me._onTween.fire();} +mgr.stop(me);}});var onStart=function(){var me=this,attr;me.onStart.fire();me.runAttrs={};for(attr in this.attributes){this.setRunAttr(attr);} +me.isAnimated=true;me.startTime=now();actualFrames=0;};var onTween=function(){var me=this;me.onTween.fire({duration:now()-me.startTime,curFrame:me.curFrame});var ra=me.runAttrs;for(var attr in ra){this.setAttr(attr,me.doMethod(attr,ra[attr].start,ra[attr].end),ra[attr].unit);} +++actualFrames;};var onComplete=function(){var me=this,actual=(now()-me.startTime)/1000,data={duration:actual,frames:actualFrames,fps:actualFrames/actual};me.isAnimated=false;actualFrames=0;me.onComplete.fire(data);};me.onStart=new Ext.util.Event(me);me.onTween=new Ext.util.Event(me);me.onComplete=new Ext.util.Event(me);(me._onStart=new Ext.util.Event(me)).addListener(onStart);(me._onTween=new Ext.util.Event(me)).addListener(onTween);(me._onComplete=new Ext.util.Event(me)).addListener(onComplete);}};Ext.lib.AnimMgr=new function(){var me=this,thread=null,queue=[],tweenCount=0;Ext.apply(me,{fps:1000,delay:1,registerElement:function(tween){queue.push(tween);++tweenCount;tween._onStart.fire();me.start();},unRegister:function(tween,index){tween._onComplete.fire();index=index||getIndex(tween);if(index!=-1){queue.splice(index,1);} +if(--tweenCount<=0){me.stop();}},start:function(){if(thread===null){thread=setInterval(me.run,me.delay);}},stop:function(tween){if(!tween){clearInterval(thread);for(var i=0,len=queue.length;i<len;++i){if(queue[0].isAnimated){me.unRegister(queue[0],0);}} +queue=[];thread=null;tweenCount=0;}else{me.unRegister(tween);}},run:function(){var tf,i,len,tween;for(i=0,len=queue.length;i<len;i++){tween=queue[i];if(tween&&tween.isAnimated){tf=tween.totalFrames;if(tween.curFrame<tf||tf===null){++tween.curFrame;if(tween.useSec){correctFrame(tween);} +tween._onTween.fire();}else{me.stop(tween);}}}}});var getIndex=function(anim){var i,len;for(i=0,len=queue.length;i<len;i++){if(queue[i]===anim){return i;}} +return-1;};var correctFrame=function(tween){var frames=tween.totalFrames,frame=tween.curFrame,duration=tween.duration,expected=(frame*duration*1000/frames),elapsed=(now()-tween.startTime),tweak=0;if(elapsed<duration*1000){tweak=Math.round((elapsed/expected-1)*frame);}else{tweak=frames-(frame+1);} +if(tweak>0&&isFinite(tweak)){if(tween.curFrame+tweak>=frames){tweak=frames-(frame+1);} +tween.curFrame+=tweak;}};};EXTLIB.Bezier=new function(){this.getPosition=function(points,t){var n=points.length,tmp=[],c=1-t,i,j;for(i=0;i<n;++i){tmp[i]=[points[i][0],points[i][1]];} +for(j=1;j<n;++j){for(i=0;i<n-j;++i){tmp[i][0]=c*tmp[i][0]+t*tmp[parseInt(i+1,10)][0];tmp[i][1]=c*tmp[i][1]+t*tmp[parseInt(i+1,10)][1];}} +return[tmp[0][0],tmp[0][1]];};};EXTLIB.Easing={easeNone:function(t,b,c,d){return c*t/d+b;},easeIn:function(t,b,c,d){return c*(t/=d)*t+b;},easeOut:function(t,b,c,d){return-c*(t/=d)*(t-2)+b;}};(function(){EXTLIB.Motion=function(el,attributes,duration,method){if(el){EXTLIB.Motion.superclass.constructor.call(this,el,attributes,duration,method);}};Ext.extend(EXTLIB.Motion,Ext.lib.AnimBase);var superclass=EXTLIB.Motion.superclass,pointsRe=/^points$/i;Ext.apply(EXTLIB.Motion.prototype,{setAttr:function(attr,val,unit){var me=this,setAttr=superclass.setAttr;if(pointsRe.test(attr)){unit=unit||'px';setAttr.call(me,'left',val[0],unit);setAttr.call(me,'top',val[1],unit);}else{setAttr.call(me,attr,val,unit);}},getAttr:function(attr){var me=this,getAttr=superclass.getAttr;return pointsRe.test(attr)?[getAttr.call(me,'left'),getAttr.call(me,'top')]:getAttr.call(me,attr);},doMethod:function(attr,start,end){var me=this;return pointsRe.test(attr)?EXTLIB.Bezier.getPosition(me.runAttrs[attr],me.method(me.curFrame,0,100,me.totalFrames)/100):superclass.doMethod.call(me,attr,start,end);},setRunAttr:function(attr){if(pointsRe.test(attr)){var me=this,el=this.el,points=this.attributes.points,control=points.control||[],from=points.from,to=points.to,by=points.by,DOM=EXTLIB.Dom,start,i,end,len,ra;if(control.length>0&&!Ext.isArray(control[0])){control=[control];}else{} +Ext.fly(el,'_anim').position();DOM.setXY(el,isset(from)?from:DOM.getXY(el));start=me.getAttr('points');if(isset(to)){end=translateValues.call(me,to,start);for(i=0,len=control.length;i<len;++i){control[i]=translateValues.call(me,control[i],start);}}else if(isset(by)){end=[start[0]+by[0],start[1]+by[1]];for(i=0,len=control.length;i<len;++i){control[i]=[start[0]+control[i][0],start[1]+control[i][1]];}} +ra=this.runAttrs[attr]=[start];if(control.length>0){ra=ra.concat(control);} +ra[ra.length]=end;}else{superclass.setRunAttr.call(this,attr);}}});var translateValues=function(val,start){var pageXY=EXTLIB.Dom.getXY(this.el);return[val[0]-pageXY[0]+start[0],val[1]-pageXY[1]+start[1]];};})();})();(function(){var abs=Math.abs,pi=Math.PI,asin=Math.asin,pow=Math.pow,sin=Math.sin,EXTLIB=Ext.lib;Ext.apply(EXTLIB.Easing,{easeBoth:function(t,b,c,d){return((t/=d/2)<1)?c/2*t*t+b:-c/2*((--t)*(t-2)-1)+b;},easeInStrong:function(t,b,c,d){return c*(t/=d)*t*t*t+b;},easeOutStrong:function(t,b,c,d){return-c*((t=t/d-1)*t*t*t-1)+b;},easeBothStrong:function(t,b,c,d){return((t/=d/2)<1)?c/2*t*t*t*t+b:-c/2*((t-=2)*t*t*t-2)+b;},elasticIn:function(t,b,c,d,a,p){if(t==0||(t/=d)==1){return t==0?b:b+c;} +p=p||(d*.3);var s;if(a>=abs(c)){s=p/(2*pi)*asin(c/a);}else{a=c;s=p/4;} +return-(a*pow(2,10*(t-=1))*sin((t*d-s)*(2*pi)/p))+b;},elasticOut:function(t,b,c,d,a,p){if(t==0||(t/=d)==1){return t==0?b:b+c;} +p=p||(d*.3);var s;if(a>=abs(c)){s=p/(2*pi)*asin(c/a);}else{a=c;s=p/4;} +return a*pow(2,-10*t)*sin((t*d-s)*(2*pi)/p)+c+b;},elasticBoth:function(t,b,c,d,a,p){if(t==0||(t/=d/2)==2){return t==0?b:b+c;} +p=p||(d*(.3*1.5));var s;if(a>=abs(c)){s=p/(2*pi)*asin(c/a);}else{a=c;s=p/4;} +return t<1?-.5*(a*pow(2,10*(t-=1))*sin((t*d-s)*(2*pi)/p))+b:a*pow(2,-10*(t-=1))*sin((t*d-s)*(2*pi)/p)*.5+c+b;},backIn:function(t,b,c,d,s){s=s||1.70158;return c*(t/=d)*t*((s+1)*t-s)+b;},backOut:function(t,b,c,d,s){if(!s){s=1.70158;} +return c*((t=t/d-1)*t*((s+1)*t+s)+1)+b;},backBoth:function(t,b,c,d,s){s=s||1.70158;return((t/=d/2)<1)?c/2*(t*t*(((s*=(1.525))+1)*t-s))+b:c/2*((t-=2)*t*(((s*=(1.525))+1)*t+s)+2)+b;},bounceIn:function(t,b,c,d){return c-EXTLIB.Easing.bounceOut(d-t,0,c,d)+b;},bounceOut:function(t,b,c,d){if((t/=d)<(1/2.75)){return c*(7.5625*t*t)+b;}else if(t<(2/2.75)){return c*(7.5625*(t-=(1.5/2.75))*t+.75)+b;}else if(t<(2.5/2.75)){return c*(7.5625*(t-=(2.25/2.75))*t+.9375)+b;} +return c*(7.5625*(t-=(2.625/2.75))*t+.984375)+b;},bounceBoth:function(t,b,c,d){return(t<d/2)?EXTLIB.Easing.bounceIn(t*2,0,c,d)*.5+b:EXTLIB.Easing.bounceOut(t*2-d,0,c,d)*.5+c*.5+b;}});})();(function(){var EXTLIB=Ext.lib;EXTLIB.Anim.color=function(el,args,duration,easing,cb,scope){return EXTLIB.Anim.run(el,args,duration,easing,cb,scope,EXTLIB.ColorAnim);};EXTLIB.ColorAnim=function(el,attributes,duration,method){EXTLIB.ColorAnim.superclass.constructor.call(this,el,attributes,duration,method);};Ext.extend(EXTLIB.ColorAnim,EXTLIB.AnimBase);var superclass=EXTLIB.ColorAnim.superclass,colorRE=/color$/i,transparentRE=/^transparent|rgba\(0, 0, 0, 0\)$/,rgbRE=/^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,hexRE=/^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,hex3RE=/^#?([0-9A-F]{1})([0-9A-F]{1})([0-9A-F]{1})$/i,isset=function(v){return typeof v!=='undefined';};function parseColor(s){var pi=parseInt,base,out=null,c;if(s.length==3){return s;} +Ext.each([hexRE,rgbRE,hex3RE],function(re,idx){base=(idx%2==0)?16:10;c=re.exec(s);if(c&&c.length==4){out=[pi(c[1],base),pi(c[2],base),pi(c[3],base)];return false;}});return out;} +Ext.apply(EXTLIB.ColorAnim.prototype,{getAttr:function(attr){var me=this,el=me.el,val;if(colorRE.test(attr)){while(el&&transparentRE.test(val=Ext.fly(el).getStyle(attr))){el=el.parentNode;val="fff";}}else{val=superclass.getAttr.call(me,attr);} +return val;},doMethod:function(attr,start,end){var me=this,val,floor=Math.floor,i,len,v;if(colorRE.test(attr)){val=[];end=end||[];for(i=0,len=start.length;i<len;i++){v=start[i];val[i]=superclass.doMethod.call(me,attr,v,end[i]);} +val='rgb('+floor(val[0])+','+floor(val[1])+','+floor(val[2])+')';}else{val=superclass.doMethod.call(me,attr,start,end);} +return val;},setRunAttr:function(attr){var me=this,a=me.attributes[attr],to=a.to,by=a.by,ra;superclass.setRunAttr.call(me,attr);ra=me.runAttrs[attr];if(colorRE.test(attr)){var start=parseColor(ra.start),end=parseColor(ra.end);if(!isset(to)&&isset(by)){end=parseColor(by);for(var i=0,len=start.length;i<len;i++){end[i]=start[i]+end[i];}} +ra.start=start;ra.end=end;}}});})();(function(){var EXTLIB=Ext.lib;EXTLIB.Anim.scroll=function(el,args,duration,easing,cb,scope){return EXTLIB.Anim.run(el,args,duration,easing,cb,scope,EXTLIB.Scroll);};EXTLIB.Scroll=function(el,attributes,duration,method){if(el){EXTLIB.Scroll.superclass.constructor.call(this,el,attributes,duration,method);}};Ext.extend(EXTLIB.Scroll,EXTLIB.ColorAnim);var superclass=EXTLIB.Scroll.superclass,SCROLL='scroll';Ext.apply(EXTLIB.Scroll.prototype,{doMethod:function(attr,start,end){var val,me=this,curFrame=me.curFrame,totalFrames=me.totalFrames;if(attr==SCROLL){val=[me.method(curFrame,start[0],end[0]-start[0],totalFrames),me.method(curFrame,start[1],end[1]-start[1],totalFrames)];}else{val=superclass.doMethod.call(me,attr,start,end);} +return val;},getAttr:function(attr){var me=this;if(attr==SCROLL){return[me.el.scrollLeft,me.el.scrollTop];}else{return superclass.getAttr.call(me,attr);}},setAttr:function(attr,val,unit){var me=this;if(attr==SCROLL){me.el.scrollLeft=val[0];me.el.scrollTop=val[1];}else{superclass.setAttr.call(me,attr,val,unit);}}});})();if(Ext.isIE){function fnCleanUp(){var p=Function.prototype;delete p.createSequence;delete p.defer;delete p.createDelegate;delete p.createCallback;delete p.createInterceptor;window.detachEvent("onunload",fnCleanUp);} +window.attachEvent("onunload",fnCleanUp);}})();(function(){var EXTUTIL=Ext.util,EACH=Ext.each,TRUE=true,FALSE=false;EXTUTIL.Observable=function(){var me=this,e=me.events;if(me.listeners){me.on(me.listeners);delete me.listeners;} +me.events=e||{};};EXTUTIL.Observable.prototype={filterOptRe:/^(?:scope|delay|buffer|single)$/,fireEvent:function(){var a=Array.prototype.slice.call(arguments,0),ename=a[0].toLowerCase(),me=this,ret=TRUE,ce=me.events[ename],cc,q,c;if(me.eventsSuspended===TRUE){if(q=me.eventQueue){q.push(a);}} +else if(typeof ce=='object'){if(ce.bubble){if(ce.fire.apply(ce,a.slice(1))===FALSE){return FALSE;} +c=me.getBubbleTarget&&me.getBubbleTarget();if(c&&c.enableBubble){cc=c.events[ename];if(!cc||typeof cc!='object'||!cc.bubble){c.enableBubble(ename);} +return c.fireEvent.apply(c,a);}} +else{a.shift();ret=ce.fire.apply(ce,a);}} +return ret;},addListener:function(eventName,fn,scope,o){var me=this,e,oe,ce;if(typeof eventName=='object'){o=eventName;for(e in o){oe=o[e];if(!me.filterOptRe.test(e)){me.addListener(e,oe.fn||oe,oe.scope||o.scope,oe.fn?oe:o);}}}else{eventName=eventName.toLowerCase();ce=me.events[eventName]||TRUE;if(typeof ce=='boolean'){me.events[eventName]=ce=new EXTUTIL.Event(me,eventName);} +ce.addListener(fn,scope,typeof o=='object'?o:{});}},removeListener:function(eventName,fn,scope){var ce=this.events[eventName.toLowerCase()];if(typeof ce=='object'){ce.removeListener(fn,scope);}},purgeListeners:function(){var events=this.events,evt,key;for(key in events){evt=events[key];if(typeof evt=='object'){evt.clearListeners();}}},addEvents:function(o){var me=this;me.events=me.events||{};if(typeof o=='string'){var a=arguments,i=a.length;while(i--){me.events[a[i]]=me.events[a[i]]||TRUE;}}else{Ext.applyIf(me.events,o);}},hasListener:function(eventName){var e=this.events[eventName.toLowerCase()];return typeof e=='object'&&e.listeners.length>0;},suspendEvents:function(queueSuspended){this.eventsSuspended=TRUE;if(queueSuspended&&!this.eventQueue){this.eventQueue=[];}},resumeEvents:function(){var me=this,queued=me.eventQueue||[];me.eventsSuspended=FALSE;delete me.eventQueue;EACH(queued,function(e){me.fireEvent.apply(me,e);});}};var OBSERVABLE=EXTUTIL.Observable.prototype;OBSERVABLE.on=OBSERVABLE.addListener;OBSERVABLE.un=OBSERVABLE.removeListener;EXTUTIL.Observable.releaseCapture=function(o){o.fireEvent=OBSERVABLE.fireEvent;};function createTargeted(h,o,scope){return function(){if(o.target==arguments[0]){h.apply(scope,Array.prototype.slice.call(arguments,0));}};};function createBuffered(h,o,l,scope){l.task=new EXTUTIL.DelayedTask();return function(){l.task.delay(o.buffer,h,scope,Array.prototype.slice.call(arguments,0));};};function createSingle(h,e,fn,scope){return function(){e.removeListener(fn,scope);return h.apply(scope,arguments);};};function createDelayed(h,o,l,scope){return function(){var task=new EXTUTIL.DelayedTask(),args=Array.prototype.slice.call(arguments,0);if(!l.tasks){l.tasks=[];} +l.tasks.push(task);task.delay(o.delay||10,function(){l.tasks.remove(task);h.apply(scope,args);},scope);};};EXTUTIL.Event=function(obj,name){this.name=name;this.obj=obj;this.listeners=[];};EXTUTIL.Event.prototype={addListener:function(fn,scope,options){var me=this,l;scope=scope||me.obj;if(!me.isListening(fn,scope)){l=me.createListener(fn,scope,options);if(me.firing){me.listeners=me.listeners.slice(0);} +me.listeners.push(l);}},createListener:function(fn,scope,o){o=o||{};scope=scope||this.obj;var l={fn:fn,scope:scope,options:o},h=fn;if(o.target){h=createTargeted(h,o,scope);} +if(o.delay){h=createDelayed(h,o,l,scope);} +if(o.single){h=createSingle(h,this,fn,scope);} +if(o.buffer){h=createBuffered(h,o,l,scope);} +l.fireFn=h;return l;},findListener:function(fn,scope){var list=this.listeners,i=list.length,l;scope=scope||this.obj;while(i--){l=list[i];if(l){if(l.fn==fn&&l.scope==scope){return i;}}} +return-1;},isListening:function(fn,scope){return this.findListener(fn,scope)!=-1;},removeListener:function(fn,scope){var index,l,k,me=this,ret=FALSE;if((index=me.findListener(fn,scope))!=-1){if(me.firing){me.listeners=me.listeners.slice(0);} +l=me.listeners[index];if(l.task){l.task.cancel();delete l.task;} +k=l.tasks&&l.tasks.length;if(k){while(k--){l.tasks[k].cancel();} +delete l.tasks;} +me.listeners.splice(index,1);ret=TRUE;} +return ret;},clearListeners:function(){var me=this,l=me.listeners,i=l.length;while(i--){me.removeListener(l[i].fn,l[i].scope);}},fire:function(){var me=this,listeners=me.listeners,len=listeners.length,i=0,l;if(len>0){me.firing=TRUE;var args=Array.prototype.slice.call(arguments,0);for(;i<len;i++){l=listeners[i];if(l&&l.fireFn.apply(l.scope||me.obj||window,args)===FALSE){return(me.firing=FALSE);}}} +me.firing=FALSE;return TRUE;}};})();Ext.DomHelper=function(){var tempTableEl=null,emptyTags=/^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,tableRe=/^table|tbody|tr|td$/i,confRe=/tag|children|cn|html$/i,tableElRe=/td|tr|tbody/i,cssRe=/([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,endRe=/end/i,pub,afterbegin='afterbegin',afterend='afterend',beforebegin='beforebegin',beforeend='beforeend',ts='<table>',te='</table>',tbs=ts+'<tbody>',tbe='</tbody>'+te,trs=tbs+'<tr>',tre='</tr>'+tbe;function doInsert(el,o,returnElement,pos,sibling,append){var newNode=pub.insertHtml(pos,Ext.getDom(el),createHtml(o));return returnElement?Ext.get(newNode,true):newNode;} +function createHtml(o){var b='',attr,val,key,cn;if(typeof o=="string"){b=o;}else if(Ext.isArray(o)){for(var i=0;i<o.length;i++){if(o[i]){b+=createHtml(o[i]);}};}else{b+='<'+(o.tag=o.tag||'div');for(attr in o){val=o[attr];if(!confRe.test(attr)){if(typeof val=="object"){b+=' '+attr+'="';for(key in val){b+=key+':'+val[key]+';';};b+='"';}else{b+=' '+({cls:'class',htmlFor:'for'}[attr]||attr)+'="'+val+'"';}}};if(emptyTags.test(o.tag)){b+='/>';}else{b+='>';if((cn=o.children||o.cn)){b+=createHtml(cn);}else if(o.html){b+=o.html;} +b+='</'+o.tag+'>';}} +return b;} +function ieTable(depth,s,h,e){tempTableEl.innerHTML=[s,h,e].join('');var i=-1,el=tempTableEl,ns;while(++i<depth){el=el.firstChild;} +if(ns=el.nextSibling){var df=document.createDocumentFragment();while(el){ns=el.nextSibling;df.appendChild(el);el=ns;} +el=df;} +return el;} +function insertIntoTable(tag,where,el,html){var node,before;tempTableEl=tempTableEl||document.createElement('div');if(tag=='td'&&(where==afterbegin||where==beforeend)||!tableElRe.test(tag)&&(where==beforebegin||where==afterend)){return;} +before=where==beforebegin?el:where==afterend?el.nextSibling:where==afterbegin?el.firstChild:null;if(where==beforebegin||where==afterend){el=el.parentNode;} +if(tag=='td'||(tag=='tr'&&(where==beforeend||where==afterbegin))){node=ieTable(4,trs,html,tre);}else if((tag=='tbody'&&(where==beforeend||where==afterbegin))||(tag=='tr'&&(where==beforebegin||where==afterend))){node=ieTable(3,tbs,html,tbe);}else{node=ieTable(2,ts,html,te);} +el.insertBefore(node,before);return node;} +pub={markup:function(o){return createHtml(o);},applyStyles:function(el,styles){if(styles){var matches;el=Ext.fly(el);if(typeof styles=="function"){styles=styles.call();} +if(typeof styles=="string"){cssRe.lastIndex=0;while((matches=cssRe.exec(styles))){el.setStyle(matches[1],matches[2]);}}else if(typeof styles=="object"){el.setStyle(styles);}}},insertHtml:function(where,el,html){var hash={},hashVal,setStart,range,frag,rangeEl,rs;where=where.toLowerCase();hash[beforebegin]=['BeforeBegin','previousSibling'];hash[afterend]=['AfterEnd','nextSibling'];if(el.insertAdjacentHTML){if(tableRe.test(el.tagName)&&(rs=insertIntoTable(el.tagName.toLowerCase(),where,el,html))){return rs;} +hash[afterbegin]=['AfterBegin','firstChild'];hash[beforeend]=['BeforeEnd','lastChild'];if((hashVal=hash[where])){el.insertAdjacentHTML(hashVal[0],html);return el[hashVal[1]];}}else{range=el.ownerDocument.createRange();setStart='setStart'+(endRe.test(where)?'After':'Before');if(hash[where]){range[setStart](el);frag=range.createContextualFragment(html);el.parentNode.insertBefore(frag,where==beforebegin?el:el.nextSibling);return el[(where==beforebegin?'previous':'next')+'Sibling'];}else{rangeEl=(where==afterbegin?'first':'last')+'Child';if(el.firstChild){range[setStart](el[rangeEl]);frag=range.createContextualFragment(html);if(where==afterbegin){el.insertBefore(frag,el.firstChild);}else{el.appendChild(frag);}}else{el.innerHTML=html;} +return el[rangeEl];}} +throw'Illegal insertion point -> "'+where+'"';},insertBefore:function(el,o,returnElement){return doInsert(el,o,returnElement,beforebegin);},insertAfter:function(el,o,returnElement){return doInsert(el,o,returnElement,afterend,'nextSibling');},insertFirst:function(el,o,returnElement){return doInsert(el,o,returnElement,afterbegin,'firstChild');},append:function(el,o,returnElement){return doInsert(el,o,returnElement,beforeend,'',true);},overwrite:function(el,o,returnElement){el=Ext.getDom(el);el.innerHTML=createHtml(o);return returnElement?Ext.get(el.firstChild):el.firstChild;},createHtml:createHtml};return pub;}();Ext.Template=function(html){var me=this,a=arguments,buf=[],v;if(Ext.isArray(html)){html=html.join("");}else if(a.length>1){for(var i=0,len=a.length;i<len;i++){v=a[i];if(typeof v=='object'){Ext.apply(me,v);}else{buf.push(v);}};html=buf.join('');} +me.html=html;if(me.compiled){me.compile();}};Ext.Template.prototype={re:/\{([\w-]+)\}/g,applyTemplate:function(values){var me=this;return me.compiled?me.compiled(values):me.html.replace(me.re,function(m,name){return values[name]!==undefined?values[name]:"";});},set:function(html,compile){var me=this;me.html=html;me.compiled=null;return compile?me.compile():me;},compile:function(){var me=this,sep=Ext.isGecko?"+":",";function fn(m,name){name="values['"+name+"']";return"'"+sep+'('+name+" == undefined ? '' : "+name+')'+sep+"'";} +eval("this.compiled = function(values){ return "+(Ext.isGecko?"'":"['")+ +me.html.replace(/\\/g,'\\\\').replace(/(\r\n|\n)/g,'\\n').replace(/'/g,"\\'").replace(this.re,fn)+ +(Ext.isGecko?"';};":"'].join('');};"));return me;},insertFirst:function(el,values,returnElement){return this.doInsert('afterBegin',el,values,returnElement);},insertBefore:function(el,values,returnElement){return this.doInsert('beforeBegin',el,values,returnElement);},insertAfter:function(el,values,returnElement){return this.doInsert('afterEnd',el,values,returnElement);},append:function(el,values,returnElement){return this.doInsert('beforeEnd',el,values,returnElement);},doInsert:function(where,el,values,returnEl){el=Ext.getDom(el);var newNode=Ext.DomHelper.insertHtml(where,el,this.applyTemplate(values));return returnEl?Ext.get(newNode,true):newNode;},overwrite:function(el,values,returnElement){el=Ext.getDom(el);el.innerHTML=this.applyTemplate(values);return returnElement?Ext.get(el.firstChild,true):el.firstChild;}};Ext.Template.prototype.apply=Ext.Template.prototype.applyTemplate;Ext.Template.from=function(el,config){el=Ext.getDom(el);return new Ext.Template(el.value||el.innerHTML,config||'');};Ext.DomQuery=function(){var cache={},simpleCache={},valueCache={},nonSpace=/\S/,trimRe=/^\s+|\s+$/g,tplRe=/\{(\d+)\}/g,modeRe=/^(\s?[\/>+~]\s?|\s|$)/,tagTokenRe=/^(#)?([\w-\*]+)/,nthRe=/(\d*)n\+?(\d*)/,nthRe2=/\D/,isIE=window.ActiveXObject?true:false,key=30803;eval("var batch = 30803;");function child(parent,index){var i=0,n=parent.firstChild;while(n){if(n.nodeType==1){if(++i==index){return n;}} +n=n.nextSibling;} +return null;} +function next(n){while((n=n.nextSibling)&&n.nodeType!=1);return n;} +function prev(n){while((n=n.previousSibling)&&n.nodeType!=1);return n;} +function children(parent){var n=parent.firstChild,nodeIndex=-1,nextNode;while(n){nextNode=n.nextSibling;if(n.nodeType==3&&!nonSpace.test(n.nodeValue)){parent.removeChild(n);}else{n.nodeIndex=++nodeIndex;} +n=nextNode;} +return this;} +function byClassName(nodeSet,cls){if(!cls){return nodeSet;} +var result=[],ri=-1;for(var i=0,ci;ci=nodeSet[i];i++){if((' '+ci.className+' ').indexOf(cls)!=-1){result[++ri]=ci;}} +return result;};function attrValue(n,attr){if(!n.tagName&&typeof n.length!="undefined"){n=n[0];} +if(!n){return null;} +if(attr=="for"){return n.htmlFor;} +if(attr=="class"||attr=="className"){return n.className;} +return n.getAttribute(attr)||n[attr];};function getNodes(ns,mode,tagName){var result=[],ri=-1,cs;if(!ns){return result;} +tagName=tagName||"*";if(typeof ns.getElementsByTagName!="undefined"){ns=[ns];} +if(!mode){for(var i=0,ni;ni=ns[i];i++){cs=ni.getElementsByTagName(tagName);for(var j=0,ci;ci=cs[j];j++){result[++ri]=ci;}}}else if(mode=="/"||mode==">"){var utag=tagName.toUpperCase();for(var i=0,ni,cn;ni=ns[i];i++){cn=ni.childNodes;for(var j=0,cj;cj=cn[j];j++){if(cj.nodeName==utag||cj.nodeName==tagName||tagName=='*'){result[++ri]=cj;}}}}else if(mode=="+"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)&&n.nodeType!=1);if(n&&(n.nodeName==utag||n.nodeName==tagName||tagName=='*')){result[++ri]=n;}}}else if(mode=="~"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)){if(n.nodeName==utag||n.nodeName==tagName||tagName=='*'){result[++ri]=n;}}}} +return result;} +function concat(a,b){if(b.slice){return a.concat(b);} +for(var i=0,l=b.length;i<l;i++){a[a.length]=b[i];} +return a;} +function byTag(cs,tagName){if(cs.tagName||cs==document){cs=[cs];} +if(!tagName){return cs;} +var result=[],ri=-1;tagName=tagName.toLowerCase();for(var i=0,ci;ci=cs[i];i++){if(ci.nodeType==1&&ci.tagName.toLowerCase()==tagName){result[++ri]=ci;}} +return result;} +function byId(cs,id){if(cs.tagName||cs==document){cs=[cs];} +if(!id){return cs;} +var result=[],ri=-1;for(var i=0,ci;ci=cs[i];i++){if(ci&&ci.id==id){result[++ri]=ci;return result;}} +return result;} +function byAttribute(cs,attr,value,op,custom){var result=[],ri=-1,useGetStyle=custom=="{",fn=Ext.DomQuery.operators[op],a,xml,hasXml;for(var i=0,ci;ci=cs[i];i++){if(ci.nodeType!=1){continue;} +if(!hasXml){xml=Ext.DomQuery.isXml(ci);hasXml=true;} +if(!xml){if(useGetStyle){a=Ext.DomQuery.getStyle(ci,attr);}else if(attr=="class"||attr=="className"){a=ci.className;}else if(attr=="for"){a=ci.htmlFor;}else if(attr=="href"){a=ci.getAttribute("href",2);}else{a=ci.getAttribute(attr);}}else{a=ci.getAttribute(attr);} +if((fn&&fn(a,value))||(!fn&&a)){result[++ri]=ci;}} +return result;} +function byPseudo(cs,name,value){return Ext.DomQuery.pseudos[name](cs,value);} +function nodupIEXml(cs){var d=++key,r;cs[0].setAttribute("_nodup",d);r=[cs[0]];for(var i=1,len=cs.length;i<len;i++){var c=cs[i];if(!c.getAttribute("_nodup")!=d){c.setAttribute("_nodup",d);r[r.length]=c;}} +for(var i=0,len=cs.length;i<len;i++){cs[i].removeAttribute("_nodup");} +return r;} +function nodup(cs){if(!cs){return[];} +var len=cs.length,c,i,r=cs,cj,ri=-1;if(!len||typeof cs.nodeType!="undefined"||len==1){return cs;} +if(isIE&&typeof cs[0].selectSingleNode!="undefined"){return nodupIEXml(cs);} +var d=++key;cs[0]._nodup=d;for(i=1;c=cs[i];i++){if(c._nodup!=d){c._nodup=d;}else{r=[];for(var j=0;j<i;j++){r[++ri]=cs[j];} +for(j=i+1;cj=cs[j];j++){if(cj._nodup!=d){cj._nodup=d;r[++ri]=cj;}} +return r;}} +return r;} +function quickDiffIEXml(c1,c2){var d=++key,r=[];for(var i=0,len=c1.length;i<len;i++){c1[i].setAttribute("_qdiff",d);} +for(var i=0,len=c2.length;i<len;i++){if(c2[i].getAttribute("_qdiff")!=d){r[r.length]=c2[i];}} +for(var i=0,len=c1.length;i<len;i++){c1[i].removeAttribute("_qdiff");} +return r;} +function quickDiff(c1,c2){var len1=c1.length,d=++key,r=[];if(!len1){return c2;} +if(isIE&&typeof c1[0].selectSingleNode!="undefined"){return quickDiffIEXml(c1,c2);} +for(var i=0;i<len1;i++){c1[i]._qdiff=d;} +for(var i=0,len=c2.length;i<len;i++){if(c2[i]._qdiff!=d){r[r.length]=c2[i];}} +return r;} +function quickId(ns,mode,root,id){if(ns==root){var d=root.ownerDocument||root;return d.getElementById(id);} +ns=getNodes(ns,mode,"*");return byId(ns,id);} +return{getStyle:function(el,name){return Ext.fly(el).getStyle(name);},compile:function(path,type){type=type||"select";var fn=["var f = function(root){\n var mode; ++batch; var n = root || document;\n"],mode,lastPath,matchers=Ext.DomQuery.matchers,matchersLn=matchers.length,modeMatch,lmode=path.match(modeRe);if(lmode&&lmode[1]){fn[fn.length]='mode="'+lmode[1].replace(trimRe,"")+'";';path=path.replace(lmode[1],"");} +while(path.substr(0,1)=="/"){path=path.substr(1);} +while(path&&lastPath!=path){lastPath=path;var tokenMatch=path.match(tagTokenRe);if(type=="select"){if(tokenMatch){if(tokenMatch[1]=="#"){fn[fn.length]='n = quickId(n, mode, root, "'+tokenMatch[2]+'");';}else{fn[fn.length]='n = getNodes(n, mode, "'+tokenMatch[2]+'");';} +path=path.replace(tokenMatch[0],"");}else if(path.substr(0,1)!='@'){fn[fn.length]='n = getNodes(n, mode, "*");';}}else{if(tokenMatch){if(tokenMatch[1]=="#"){fn[fn.length]='n = byId(n, "'+tokenMatch[2]+'");';}else{fn[fn.length]='n = byTag(n, "'+tokenMatch[2]+'");';} +path=path.replace(tokenMatch[0],"");}} +while(!(modeMatch=path.match(modeRe))){var matched=false;for(var j=0;j<matchersLn;j++){var t=matchers[j];var m=path.match(t.re);if(m){fn[fn.length]=t.select.replace(tplRe,function(x,i){return m[i];});path=path.replace(m[0],"");matched=true;break;}} +if(!matched){throw'Error parsing selector, parsing failed at "'+path+'"';}} +if(modeMatch[1]){fn[fn.length]='mode="'+modeMatch[1].replace(trimRe,"")+'";';path=path.replace(modeMatch[1],"");}} +fn[fn.length]="return nodup(n);\n}";eval(fn.join(""));return f;},jsSelect:function(path,root,type){root=root||document;if(typeof root=="string"){root=document.getElementById(root);} +var paths=path.split(","),results=[];for(var i=0,len=paths.length;i<len;i++){var subPath=paths[i].replace(trimRe,"");if(!cache[subPath]){cache[subPath]=Ext.DomQuery.compile(subPath);if(!cache[subPath]){throw subPath+" is not a valid selector";}} +var result=cache[subPath](root);if(result&&result!=document){results=results.concat(result);}} +if(paths.length>1){return nodup(results);} +return results;},isXml:function(el){var docEl=(el?el.ownerDocument||el:0).documentElement;return docEl?docEl.nodeName!=="HTML":false;},select:document.querySelectorAll?function(path,root,type){root=root||document;if(!Ext.DomQuery.isXml(root)){try{var cs=root.querySelectorAll(path);return Ext.toArray(cs);} +catch(ex){}} +return Ext.DomQuery.jsSelect.call(this,path,root,type);}:function(path,root,type){return Ext.DomQuery.jsSelect.call(this,path,root,type);},selectNode:function(path,root){return Ext.DomQuery.select(path,root)[0];},selectValue:function(path,root,defaultValue){path=path.replace(trimRe,"");if(!valueCache[path]){valueCache[path]=Ext.DomQuery.compile(path,"select");} +var n=valueCache[path](root),v;n=n[0]?n[0]:n;if(typeof n.normalize=='function')n.normalize();v=(n&&n.firstChild?n.firstChild.nodeValue:null);return((v===null||v===undefined||v==='')?defaultValue:v);},selectNumber:function(path,root,defaultValue){var v=Ext.DomQuery.selectValue(path,root,defaultValue||0);return parseFloat(v);},is:function(el,ss){if(typeof el=="string"){el=document.getElementById(el);} +var isArray=Ext.isArray(el),result=Ext.DomQuery.filter(isArray?el:[el],ss);return isArray?(result.length==el.length):(result.length>0);},filter:function(els,ss,nonMatches){ss=ss.replace(trimRe,"");if(!simpleCache[ss]){simpleCache[ss]=Ext.DomQuery.compile(ss,"simple");} +var result=simpleCache[ss](els);return nonMatches?quickDiff(result,els):result;},matchers:[{re:/^\.([\w-]+)/,select:'n = byClassName(n, " {1} ");'},{re:/^\:([\w-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,select:'n = byPseudo(n, "{1}", "{2}");'},{re:/^(?:([\[\{])(?:@)?([\w-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]\}])/,select:'n = byAttribute(n, "{2}", "{4}", "{3}", "{1}");'},{re:/^#([\w-]+)/,select:'n = byId(n, "{1}");'},{re:/^@([\w-]+)/,select:'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'}],operators:{"=":function(a,v){return a==v;},"!=":function(a,v){return a!=v;},"^=":function(a,v){return a&&a.substr(0,v.length)==v;},"$=":function(a,v){return a&&a.substr(a.length-v.length)==v;},"*=":function(a,v){return a&&a.indexOf(v)!==-1;},"%=":function(a,v){return(a%v)==0;},"|=":function(a,v){return a&&(a==v||a.substr(0,v.length+1)==v+'-');},"~=":function(a,v){return a&&(' '+a+' ').indexOf(' '+v+' ')!=-1;}},pseudos:{"first-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.previousSibling)&&n.nodeType!=1);if(!n){r[++ri]=ci;}} +return r;},"last-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.nextSibling)&&n.nodeType!=1);if(!n){r[++ri]=ci;}} +return r;},"nth-child":function(c,a){var r=[],ri=-1,m=nthRe.exec(a=="even"&&"2n"||a=="odd"&&"2n+1"||!nthRe2.test(a)&&"n+"+a||a),f=(m[1]||1)-0,l=m[2]-0;for(var i=0,n;n=c[i];i++){var pn=n.parentNode;if(batch!=pn._batch){var j=0;for(var cn=pn.firstChild;cn;cn=cn.nextSibling){if(cn.nodeType==1){cn.nodeIndex=++j;}} +pn._batch=batch;} +if(f==1){if(l==0||n.nodeIndex==l){r[++ri]=n;}}else if((n.nodeIndex+l)%f==0){r[++ri]=n;}} +return r;},"only-child":function(c){var r=[],ri=-1;;for(var i=0,ci;ci=c[i];i++){if(!prev(ci)&&!next(ci)){r[++ri]=ci;}} +return r;},"empty":function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var cns=ci.childNodes,j=0,cn,empty=true;while(cn=cns[j]){++j;if(cn.nodeType==1||cn.nodeType==3){empty=false;break;}} +if(empty){r[++ri]=ci;}} +return r;},"contains":function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if((ci.textContent||ci.innerText||'').indexOf(v)!=-1){r[++ri]=ci;}} +return r;},"nodeValue":function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.firstChild&&ci.firstChild.nodeValue==v){r[++ri]=ci;}} +return r;},"checked":function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.checked==true){r[++ri]=ci;}} +return r;},"not":function(c,ss){return Ext.DomQuery.filter(c,ss,true);},"any":function(c,selectors){var ss=selectors.split('|'),r=[],ri=-1,s;for(var i=0,ci;ci=c[i];i++){for(var j=0;s=ss[j];j++){if(Ext.DomQuery.is(ci,s)){r[++ri]=ci;break;}}} +return r;},"odd":function(c){return this["nth-child"](c,"odd");},"even":function(c){return this["nth-child"](c,"even");},"nth":function(c,a){return c[a-1]||[];},"first":function(c){return c[0]||[];},"last":function(c){return c[c.length-1]||[];},"has":function(c,ss){var s=Ext.DomQuery.select,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(s(ss,ci).length>0){r[++ri]=ci;}} +return r;},"next":function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=next(ci);if(n&&is(n,ss)){r[++ri]=ci;}} +return r;},"prev":function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=prev(ci);if(n&&is(n,ss)){r[++ri]=ci;}} +return r;}}};}();Ext.query=Ext.DomQuery.select;Ext.util.DelayedTask=function(fn,scope,args){var me=this,id,call=function(){clearInterval(id);id=null;fn.apply(scope,args||[]);};me.delay=function(delay,newFn,newScope,newArgs){me.cancel();fn=newFn||fn;scope=newScope||scope;args=newArgs||args;id=setInterval(call,delay);};me.cancel=function(){if(id){clearInterval(id);id=null;}};};(function(){var DOC=document;Ext.Element=function(element,forceNew){var dom=typeof element=="string"?DOC.getElementById(element):element,id;if(!dom)return null;id=dom.id;if(!forceNew&&id&&Ext.elCache[id]){return Ext.elCache[id].el;} +this.dom=dom;this.id=id||Ext.id(dom);};var DH=Ext.DomHelper,El=Ext.Element,EC=Ext.elCache;El.prototype={set:function(o,useSet){var el=this.dom,attr,val,useSet=(useSet!==false)&&!!el.setAttribute;for(attr in o){if(o.hasOwnProperty(attr)){val=o[attr];if(attr=='style'){DH.applyStyles(el,val);}else if(attr=='cls'){el.className=val;}else if(useSet){el.setAttribute(attr,val);}else{el[attr]=val;}}} +return this;},defaultUnit:"px",is:function(simpleSelector){return Ext.DomQuery.is(this.dom,simpleSelector);},focus:function(defer,dom){var me=this,dom=dom||me.dom;try{if(Number(defer)){me.focus.defer(defer,null,[null,dom]);}else{dom.focus();}}catch(e){} +return me;},blur:function(){try{this.dom.blur();}catch(e){} +return this;},getValue:function(asNumber){var val=this.dom.value;return asNumber?parseInt(val,10):val;},addListener:function(eventName,fn,scope,options){Ext.EventManager.on(this.dom,eventName,fn,scope||this,options);return this;},removeListener:function(eventName,fn,scope){Ext.EventManager.removeListener(this.dom,eventName,fn,scope||this);return this;},removeAllListeners:function(){Ext.EventManager.removeAll(this.dom);return this;},purgeAllListeners:function(){Ext.EventManager.purgeElement(this,true);return this;},addUnits:function(size){if(size===""||size=="auto"||size===undefined){size=size||'';}else if(!isNaN(size)||!unitPattern.test(size)){size=size+(this.defaultUnit||'px');} +return size;},load:function(url,params,cb){Ext.Ajax.request(Ext.apply({params:params,url:url.url||url,callback:cb,el:this.dom,indicatorText:url.indicatorText||''},Ext.isObject(url)?url:{}));return this;},isBorderBox:function(){return Ext.isBorderBox||Ext.isForcedBorderBox||noBoxAdjust[(this.dom.tagName||"").toLowerCase()];},remove:function(){var me=this,dom=me.dom;if(dom){delete me.dom;Ext.removeNode(dom);}},hover:function(overFn,outFn,scope,options){var me=this;me.on('mouseenter',overFn,scope||me.dom,options);me.on('mouseleave',outFn,scope||me.dom,options);return me;},contains:function(el){return!el?false:Ext.lib.Dom.isAncestor(this.dom,el.dom?el.dom:el);},getAttributeNS:function(ns,name){return this.getAttribute(name,ns);},getAttribute:Ext.isIE?function(name,ns){var d=this.dom,type=typeof d[ns+":"+name];if(['undefined','unknown'].indexOf(type)==-1){return d[ns+":"+name];} +return d[name];}:function(name,ns){var d=this.dom;return d.getAttributeNS(ns,name)||d.getAttribute(ns+":"+name)||d.getAttribute(name)||d[name];},update:function(html){if(this.dom){this.dom.innerHTML=html;} +return this;}};var ep=El.prototype;El.addMethods=function(o){Ext.apply(ep,o);};ep.on=ep.addListener;ep.un=ep.removeListener;ep.autoBoxAdjust=true;var unitPattern=/\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,docEl;El.get=function(el){var ex,elm,id;if(!el){return null;} +if(typeof el=="string"){if(!(elm=DOC.getElementById(el))){return null;} +if(EC[el]&&EC[el].el){ex=EC[el].el;ex.dom=elm;}else{ex=El.addToCache(new El(elm));} +return ex;}else if(el.tagName){if(!(id=el.id)){id=Ext.id(el);} +if(EC[id]&&EC[id].el){ex=EC[id].el;ex.dom=el;}else{ex=El.addToCache(new El(el));} +return ex;}else if(el instanceof El){if(el!=docEl){if(Ext.isIE&&(el.id==undefined||el.id=='')){el.dom=el.dom;}else{el.dom=DOC.getElementById(el.id)||el.dom;}} +return el;}else if(el.isComposite){return el;}else if(Ext.isArray(el)){return El.select(el);}else if(el==DOC){if(!docEl){var f=function(){};f.prototype=El.prototype;docEl=new f();docEl.dom=DOC;} +return docEl;} +return null;};El.addToCache=function(el,id){id=id||el.id;EC[id]={el:el,data:{},events:{}};return el;};El.data=function(el,key,value){el=El.get(el);if(!el){return null;} +var c=EC[el.id].data;if(arguments.length==2){return c[key];}else{return(c[key]=value);}};function garbageCollect(){if(!Ext.enableGarbageCollector){clearInterval(El.collectorThreadId);}else{var eid,el,d,o;for(eid in EC){o=EC[eid];if(o.skipGC){continue;} +el=o.el;d=el.dom;if(!d||!d.parentNode||(!d.offsetParent&&!DOC.getElementById(eid))){if(Ext.enableListenerCollection){Ext.EventManager.removeAll(d);} +delete EC[eid];}} +if(Ext.isIE){var t={};for(eid in EC){t[eid]=EC[eid];} +EC=Ext.elCache=t;}}} +El.collectorThreadId=setInterval(garbageCollect,30000);var flyFn=function(){};flyFn.prototype=El.prototype;El.Flyweight=function(dom){this.dom=dom;};El.Flyweight.prototype=new flyFn();El.Flyweight.prototype.isFlyweight=true;El._flyweights={};El.fly=function(el,named){var ret=null;named=named||'_global';if(el=Ext.getDom(el)){(El._flyweights[named]=El._flyweights[named]||new El.Flyweight()).dom=el;ret=El._flyweights[named];} +return ret;};Ext.get=El.get;Ext.fly=El.fly;var noBoxAdjust=Ext.isStrict?{select:1}:{input:1,select:1,textarea:1};if(Ext.isIE||Ext.isGecko){noBoxAdjust['button']=1;}})();Ext.Element.addMethods(function(){var PARENTNODE='parentNode',NEXTSIBLING='nextSibling',PREVIOUSSIBLING='previousSibling',DQ=Ext.DomQuery,GET=Ext.get;return{findParent:function(simpleSelector,maxDepth,returnEl){var p=this.dom,b=document.body,depth=0,stopEl;if(Ext.isGecko&&Object.prototype.toString.call(p)=='[object XULElement]'){return null;} +maxDepth=maxDepth||50;if(isNaN(maxDepth)){stopEl=Ext.getDom(maxDepth);maxDepth=Number.MAX_VALUE;} +while(p&&p.nodeType==1&&depth<maxDepth&&p!=b&&p!=stopEl){if(DQ.is(p,simpleSelector)){return returnEl?GET(p):p;} +depth++;p=p.parentNode;} +return null;},findParentNode:function(simpleSelector,maxDepth,returnEl){var p=Ext.fly(this.dom.parentNode,'_internal');return p?p.findParent(simpleSelector,maxDepth,returnEl):null;},up:function(simpleSelector,maxDepth){return this.findParentNode(simpleSelector,maxDepth,true);},select:function(selector){return Ext.Element.select(selector,this.dom);},query:function(selector){return DQ.select(selector,this.dom);},child:function(selector,returnDom){var n=DQ.selectNode(selector,this.dom);return returnDom?n:GET(n);},down:function(selector,returnDom){var n=DQ.selectNode(" > "+selector,this.dom);return returnDom?n:GET(n);},parent:function(selector,returnDom){return this.matchNode(PARENTNODE,PARENTNODE,selector,returnDom);},next:function(selector,returnDom){return this.matchNode(NEXTSIBLING,NEXTSIBLING,selector,returnDom);},prev:function(selector,returnDom){return this.matchNode(PREVIOUSSIBLING,PREVIOUSSIBLING,selector,returnDom);},first:function(selector,returnDom){return this.matchNode(NEXTSIBLING,'firstChild',selector,returnDom);},last:function(selector,returnDom){return this.matchNode(PREVIOUSSIBLING,'lastChild',selector,returnDom);},matchNode:function(dir,start,selector,returnDom){var n=this.dom[start];while(n){if(n.nodeType==1&&(!selector||DQ.is(n,selector))){return!returnDom?GET(n):n;} +n=n[dir];} +return null;}};}());Ext.Element.addMethods(function(){var GETDOM=Ext.getDom,GET=Ext.get,DH=Ext.DomHelper;return{appendChild:function(el){return GET(el).appendTo(this);},appendTo:function(el){GETDOM(el).appendChild(this.dom);return this;},insertBefore:function(el){(el=GETDOM(el)).parentNode.insertBefore(this.dom,el);return this;},insertAfter:function(el){(el=GETDOM(el)).parentNode.insertBefore(this.dom,el.nextSibling);return this;},insertFirst:function(el,returnDom){el=el||{};if(el.nodeType||el.dom||typeof el=='string'){el=GETDOM(el);this.dom.insertBefore(el,this.dom.firstChild);return!returnDom?GET(el):el;}else{return this.createChild(el,this.dom.firstChild,returnDom);}},replace:function(el){el=GET(el);this.insertBefore(el);el.remove();return this;},replaceWith:function(el){var me=this;if(el.nodeType||el.dom||typeof el=='string'){el=GETDOM(el);me.dom.parentNode.insertBefore(el,me.dom);}else{el=DH.insertBefore(me.dom,el);} +delete Ext.elCache[me.id];Ext.removeNode(me.dom);me.id=Ext.id(me.dom=el);Ext.Element.addToCache(me.isFlyweight?new Ext.Element(me.dom):me);return me;},createChild:function(config,insertBefore,returnDom){config=config||{tag:'div'};return insertBefore?DH.insertBefore(insertBefore,config,returnDom!==true):DH[!this.dom.firstChild?'overwrite':'append'](this.dom,config,returnDom!==true);},wrap:function(config,returnDom){var newEl=DH.insertBefore(this.dom,config||{tag:"div"},!returnDom);newEl.dom?newEl.dom.appendChild(this.dom):newEl.appendChild(this.dom);return newEl;},insertHtml:function(where,html,returnEl){var el=DH.insertHtml(where,this.dom,html);return returnEl?Ext.get(el):el;}};}());Ext.Element.addMethods(function(){var supports=Ext.supports,propCache={},camelRe=/(-[a-z])/gi,view=document.defaultView,opacityRe=/alpha\(opacity=(.*)\)/i,trimRe=/^\s+|\s+$/g,EL=Ext.Element,spacesRe=/\s+/,wordsRe=/\w/g,PADDING="padding",MARGIN="margin",BORDER="border",LEFT="-left",RIGHT="-right",TOP="-top",BOTTOM="-bottom",WIDTH="-width",MATH=Math,HIDDEN='hidden',ISCLIPPED='isClipped',OVERFLOW='overflow',OVERFLOWX='overflow-x',OVERFLOWY='overflow-y',ORIGINALCLIP='originalClip',borders={l:BORDER+LEFT+WIDTH,r:BORDER+RIGHT+WIDTH,t:BORDER+TOP+WIDTH,b:BORDER+BOTTOM+WIDTH},paddings={l:PADDING+LEFT,r:PADDING+RIGHT,t:PADDING+TOP,b:PADDING+BOTTOM},margins={l:MARGIN+LEFT,r:MARGIN+RIGHT,t:MARGIN+TOP,b:MARGIN+BOTTOM},data=Ext.Element.data;function camelFn(m,a){return a.charAt(1).toUpperCase();} +function chkCache(prop){return propCache[prop]||(propCache[prop]=prop=='float'?(supports.cssFloat?'cssFloat':'styleFloat'):prop.replace(camelRe,camelFn));} +return{adjustWidth:function(width){var me=this;var isNum=(typeof width=="number");if(isNum&&me.autoBoxAdjust&&!me.isBorderBox()){width-=(me.getBorderWidth("lr")+me.getPadding("lr"));} +return(isNum&&width<0)?0:width;},adjustHeight:function(height){var me=this;var isNum=(typeof height=="number");if(isNum&&me.autoBoxAdjust&&!me.isBorderBox()){height-=(me.getBorderWidth("tb")+me.getPadding("tb"));} +return(isNum&&height<0)?0:height;},addClass:function(className){var me=this,i,len,v,cls=[];if(!Ext.isArray(className)){if(typeof className=='string'&&!this.hasClass(className)){me.dom.className+=" "+className;}} +else{for(i=0,len=className.length;i<len;i++){v=className[i];if(typeof v=='string'&&(' '+me.dom.className+' ').indexOf(' '+v+' ')==-1){cls.push(v);}} +if(cls.length){me.dom.className+=" "+cls.join(" ");}} +return me;},removeClass:function(className){var me=this,i,idx,len,cls,elClasses;if(!Ext.isArray(className)){className=[className];} +if(me.dom&&me.dom.className){elClasses=me.dom.className.replace(trimRe,'').split(spacesRe);for(i=0,len=className.length;i<len;i++){cls=className[i];if(typeof cls=='string'){cls=cls.replace(trimRe,'');idx=elClasses.indexOf(cls);if(idx!=-1){elClasses.splice(idx,1);}}} +me.dom.className=elClasses.join(" ");} +return me;},radioClass:function(className){var cn=this.dom.parentNode.childNodes,v,i,len;className=Ext.isArray(className)?className:[className];for(i=0,len=cn.length;i<len;i++){v=cn[i];if(v&&v.nodeType==1){Ext.fly(v,'_internal').removeClass(className);}};return this.addClass(className);},toggleClass:function(className){return this.hasClass(className)?this.removeClass(className):this.addClass(className);},hasClass:function(className){return className&&(' '+this.dom.className+' ').indexOf(' '+className+' ')!=-1;},replaceClass:function(oldClassName,newClassName){return this.removeClass(oldClassName).addClass(newClassName);},isStyle:function(style,val){return this.getStyle(style)==val;},getStyle:function(){return view&&view.getComputedStyle?function(prop){var el=this.dom,v,cs,out,display;if(el==document){return null;} +prop=chkCache(prop);out=(v=el.style[prop])?v:(cs=view.getComputedStyle(el,""))?cs[prop]:null;if(prop=='marginRight'&&out!='0px'&&!supports.correctRightMargin){display=el.style.display;el.style.display='inline-block';out=view.getComputedStyle(el,'').marginRight;el.style.display=display;} +if(prop=='backgroundColor'&&out=='rgba(0, 0, 0, 0)'&&!supports.correctTransparentColor){out='transparent';} +return out;}:function(prop){var el=this.dom,m,cs;if(el==document)return null;if(prop=='opacity'){if(el.style.filter.match){if(m=el.style.filter.match(opacityRe)){var fv=parseFloat(m[1]);if(!isNaN(fv)){return fv?fv/100:0;}}} +return 1;} +prop=chkCache(prop);return el.style[prop]||((cs=el.currentStyle)?cs[prop]:null);};}(),getColor:function(attr,defaultValue,prefix){var v=this.getStyle(attr),color=(typeof prefix!='undefined')?prefix:'#',h;if(!v||(/transparent|inherit/.test(v))){return defaultValue;} +if(/^r/.test(v)){Ext.each(v.slice(4,v.length-1).split(','),function(s){h=parseInt(s,10);color+=(h<16?'0':'')+h.toString(16);});}else{v=v.replace('#','');color+=v.length==3?v.replace(/^(\w)(\w)(\w)$/,'$1$1$2$2$3$3'):v;} +return(color.length>5?color.toLowerCase():defaultValue);},setStyle:function(prop,value){var tmp,style;if(typeof prop!='object'){tmp={};tmp[prop]=value;prop=tmp;} +for(style in prop){value=prop[style];style=='opacity'?this.setOpacity(value):this.dom.style[chkCache(style)]=value;} +return this;},setOpacity:function(opacity,animate){var me=this,s=me.dom.style;if(!animate||!me.anim){if(Ext.isIE){var opac=opacity<1?'alpha(opacity='+opacity*100+')':'',val=s.filter.replace(opacityRe,'').replace(trimRe,'');s.zoom=1;s.filter=val+(val.length>0?' ':'')+opac;}else{s.opacity=opacity;}}else{me.anim({opacity:{to:opacity}},me.preanim(arguments,1),null,.35,'easeIn');} +return me;},clearOpacity:function(){var style=this.dom.style;if(Ext.isIE){if(!Ext.isEmpty(style.filter)){style.filter=style.filter.replace(opacityRe,'').replace(trimRe,'');}}else{style.opacity=style['-moz-opacity']=style['-khtml-opacity']='';} +return this;},getHeight:function(contentHeight){var me=this,dom=me.dom,hidden=Ext.isIE&&me.isStyle('display','none'),h=MATH.max(dom.offsetHeight,hidden?0:dom.clientHeight)||0;h=!contentHeight?h:h-me.getBorderWidth("tb")-me.getPadding("tb");return h<0?0:h;},getWidth:function(contentWidth){var me=this,dom=me.dom,hidden=Ext.isIE&&me.isStyle('display','none'),w=MATH.max(dom.offsetWidth,hidden?0:dom.clientWidth)||0;w=!contentWidth?w:w-me.getBorderWidth("lr")-me.getPadding("lr");return w<0?0:w;},setWidth:function(width,animate){var me=this;width=me.adjustWidth(width);!animate||!me.anim?me.dom.style.width=me.addUnits(width):me.anim({width:{to:width}},me.preanim(arguments,1));return me;},setHeight:function(height,animate){var me=this;height=me.adjustHeight(height);!animate||!me.anim?me.dom.style.height=me.addUnits(height):me.anim({height:{to:height}},me.preanim(arguments,1));return me;},getBorderWidth:function(side){return this.addStyles(side,borders);},getPadding:function(side){return this.addStyles(side,paddings);},clip:function(){var me=this,dom=me.dom;if(!data(dom,ISCLIPPED)){data(dom,ISCLIPPED,true);data(dom,ORIGINALCLIP,{o:me.getStyle(OVERFLOW),x:me.getStyle(OVERFLOWX),y:me.getStyle(OVERFLOWY)});me.setStyle(OVERFLOW,HIDDEN);me.setStyle(OVERFLOWX,HIDDEN);me.setStyle(OVERFLOWY,HIDDEN);} +return me;},unclip:function(){var me=this,dom=me.dom;if(data(dom,ISCLIPPED)){data(dom,ISCLIPPED,false);var o=data(dom,ORIGINALCLIP);if(o.o){me.setStyle(OVERFLOW,o.o);} +if(o.x){me.setStyle(OVERFLOWX,o.x);} +if(o.y){me.setStyle(OVERFLOWY,o.y);}} +return me;},addStyles:function(sides,styles){var ttlSize=0,sidesArr=sides.match(wordsRe),side,size,i,len=sidesArr.length;for(i=0;i<len;i++){side=sidesArr[i];size=side&&parseInt(this.getStyle(styles[side]),10);if(size){ttlSize+=MATH.abs(size);}} +return ttlSize;},margins:margins};}());(function(){var D=Ext.lib.Dom,LEFT="left",RIGHT="right",TOP="top",BOTTOM="bottom",POSITION="position",STATIC="static",RELATIVE="relative",AUTO="auto",ZINDEX="z-index";Ext.Element.addMethods({getX:function(){return D.getX(this.dom);},getY:function(){return D.getY(this.dom);},getXY:function(){return D.getXY(this.dom);},getOffsetsTo:function(el){var o=this.getXY(),e=Ext.fly(el,'_internal').getXY();return[o[0]-e[0],o[1]-e[1]];},setX:function(x,animate){return this.setXY([x,this.getY()],this.animTest(arguments,animate,1));},setY:function(y,animate){return this.setXY([this.getX(),y],this.animTest(arguments,animate,1));},setLeft:function(left){this.setStyle(LEFT,this.addUnits(left));return this;},setTop:function(top){this.setStyle(TOP,this.addUnits(top));return this;},setRight:function(right){this.setStyle(RIGHT,this.addUnits(right));return this;},setBottom:function(bottom){this.setStyle(BOTTOM,this.addUnits(bottom));return this;},setXY:function(pos,animate){var me=this;if(!animate||!me.anim){D.setXY(me.dom,pos);}else{me.anim({points:{to:pos}},me.preanim(arguments,1),'motion');} +return me;},setLocation:function(x,y,animate){return this.setXY([x,y],this.animTest(arguments,animate,2));},moveTo:function(x,y,animate){return this.setXY([x,y],this.animTest(arguments,animate,2));},getLeft:function(local){return!local?this.getX():parseInt(this.getStyle(LEFT),10)||0;},getRight:function(local){var me=this;return!local?me.getX()+me.getWidth():(me.getLeft(true)+me.getWidth())||0;},getTop:function(local){return!local?this.getY():parseInt(this.getStyle(TOP),10)||0;},getBottom:function(local){var me=this;return!local?me.getY()+me.getHeight():(me.getTop(true)+me.getHeight())||0;},position:function(pos,zIndex,x,y){var me=this;if(!pos&&me.isStyle(POSITION,STATIC)){me.setStyle(POSITION,RELATIVE);}else if(pos){me.setStyle(POSITION,pos);} +if(zIndex){me.setStyle(ZINDEX,zIndex);} +if(x||y)me.setXY([x||false,y||false]);},clearPositioning:function(value){value=value||'';this.setStyle({left:value,right:value,top:value,bottom:value,"z-index":"",position:STATIC});return this;},getPositioning:function(){var l=this.getStyle(LEFT);var t=this.getStyle(TOP);return{"position":this.getStyle(POSITION),"left":l,"right":l?"":this.getStyle(RIGHT),"top":t,"bottom":t?"":this.getStyle(BOTTOM),"z-index":this.getStyle(ZINDEX)};},setPositioning:function(pc){var me=this,style=me.dom.style;me.setStyle(pc);if(pc.right==AUTO){style.right="";} +if(pc.bottom==AUTO){style.bottom="";} +return me;},translatePoints:function(x,y){y=isNaN(x[1])?y:x[1];x=isNaN(x[0])?x:x[0];var me=this,relative=me.isStyle(POSITION,RELATIVE),o=me.getXY(),l=parseInt(me.getStyle(LEFT),10),t=parseInt(me.getStyle(TOP),10);l=!isNaN(l)?l:(relative?0:me.dom.offsetLeft);t=!isNaN(t)?t:(relative?0:me.dom.offsetTop);return{left:(x-o[0]+l),top:(y-o[1]+t)};},animTest:function(args,animate,i){return!!animate&&this.preanim?this.preanim(args,i):false;}});})();Ext.Element.addMethods({isScrollable:function(){var dom=this.dom;return dom.scrollHeight>dom.clientHeight||dom.scrollWidth>dom.clientWidth;},scrollTo:function(side,value){this.dom["scroll"+(/top/i.test(side)?"Top":"Left")]=value;return this;},getScroll:function(){var d=this.dom,doc=document,body=doc.body,docElement=doc.documentElement,l,t,ret;if(d==doc||d==body){if(Ext.isIE&&Ext.isStrict){l=docElement.scrollLeft;t=docElement.scrollTop;}else{l=window.pageXOffset;t=window.pageYOffset;} +ret={left:l||(body?body.scrollLeft:0),top:t||(body?body.scrollTop:0)};}else{ret={left:d.scrollLeft,top:d.scrollTop};} +return ret;}});Ext.Element.VISIBILITY=1;Ext.Element.DISPLAY=2;Ext.Element.OFFSETS=3;Ext.Element.ASCLASS=4;Ext.Element.visibilityCls='x-hide-nosize';Ext.Element.addMethods(function(){var El=Ext.Element,OPACITY="opacity",VISIBILITY="visibility",DISPLAY="display",HIDDEN="hidden",OFFSETS="offsets",ASCLASS="asclass",NONE="none",NOSIZE='nosize',ORIGINALDISPLAY='originalDisplay',VISMODE='visibilityMode',ISVISIBLE='isVisible',data=El.data,getDisplay=function(dom){var d=data(dom,ORIGINALDISPLAY);if(d===undefined){data(dom,ORIGINALDISPLAY,d='');} +return d;},getVisMode=function(dom){var m=data(dom,VISMODE);if(m===undefined){data(dom,VISMODE,m=1);} +return m;};return{originalDisplay:"",visibilityMode:1,setVisibilityMode:function(visMode){data(this.dom,VISMODE,visMode);return this;},animate:function(args,duration,onComplete,easing,animType){this.anim(args,{duration:duration,callback:onComplete,easing:easing},animType);return this;},anim:function(args,opt,animType,defaultDur,defaultEase,cb){animType=animType||'run';opt=opt||{};var me=this,anim=Ext.lib.Anim[animType](me.dom,args,(opt.duration||defaultDur)||.35,(opt.easing||defaultEase)||'easeOut',function(){if(cb)cb.call(me);if(opt.callback)opt.callback.call(opt.scope||me,me,opt);},me);opt.anim=anim;return anim;},preanim:function(a,i){return!a[i]?false:(typeof a[i]=='object'?a[i]:{duration:a[i+1],callback:a[i+2],easing:a[i+3]});},isVisible:function(){var me=this,dom=me.dom,visible=data(dom,ISVISIBLE);if(typeof visible=='boolean'){return visible;} +visible=!me.isStyle(VISIBILITY,HIDDEN)&&!me.isStyle(DISPLAY,NONE)&&!((getVisMode(dom)==El.ASCLASS)&&me.hasClass(me.visibilityCls||El.visibilityCls));data(dom,ISVISIBLE,visible);return visible;},setVisible:function(visible,animate){var me=this,isDisplay,isVisibility,isOffsets,isNosize,dom=me.dom,visMode=getVisMode(dom);if(typeof animate=='string'){switch(animate){case DISPLAY:visMode=El.DISPLAY;break;case VISIBILITY:visMode=El.VISIBILITY;break;case OFFSETS:visMode=El.OFFSETS;break;case NOSIZE:case ASCLASS:visMode=El.ASCLASS;break;} +me.setVisibilityMode(visMode);animate=false;} +if(!animate||!me.anim){if(visMode==El.ASCLASS){me[visible?'removeClass':'addClass'](me.visibilityCls||El.visibilityCls);}else if(visMode==El.DISPLAY){return me.setDisplayed(visible);}else if(visMode==El.OFFSETS){if(!visible){me.hideModeStyles={position:me.getStyle('position'),top:me.getStyle('top'),left:me.getStyle('left')};me.applyStyles({position:'absolute',top:'-10000px',left:'-10000px'});}else{me.applyStyles(me.hideModeStyles||{position:'',top:'',left:''});delete me.hideModeStyles;}}else{me.fixDisplay();dom.style.visibility=visible?"visible":HIDDEN;}}else{if(visible){me.setOpacity(.01);me.setVisible(true);} +me.anim({opacity:{to:(visible?1:0)}},me.preanim(arguments,1),null,.35,'easeIn',function(){visible||me.setVisible(false).setOpacity(1);});} +data(dom,ISVISIBLE,visible);return me;},hasMetrics:function(){var dom=this.dom;return this.isVisible()||(getVisMode(dom)==El.VISIBILITY);},toggle:function(animate){var me=this;me.setVisible(!me.isVisible(),me.preanim(arguments,0));return me;},setDisplayed:function(value){if(typeof value=="boolean"){value=value?getDisplay(this.dom):NONE;} +this.setStyle(DISPLAY,value);return this;},fixDisplay:function(){var me=this;if(me.isStyle(DISPLAY,NONE)){me.setStyle(VISIBILITY,HIDDEN);me.setStyle(DISPLAY,getDisplay(this.dom));if(me.isStyle(DISPLAY,NONE)){me.setStyle(DISPLAY,"block");}}},hide:function(animate){if(typeof animate=='string'){this.setVisible(false,animate);return this;} +this.setVisible(false,this.preanim(arguments,0));return this;},show:function(animate){if(typeof animate=='string'){this.setVisible(true,animate);return this;} +this.setVisible(true,this.preanim(arguments,0));return this;}};}());(function(){var NULL=null,UNDEFINED=undefined,TRUE=true,FALSE=false,SETX="setX",SETY="setY",SETXY="setXY",LEFT="left",BOTTOM="bottom",TOP="top",RIGHT="right",HEIGHT="height",WIDTH="width",POINTS="points",HIDDEN="hidden",ABSOLUTE="absolute",VISIBLE="visible",MOTION="motion",POSITION="position",EASEOUT="easeOut",flyEl=new Ext.Element.Flyweight(),queues={},getObject=function(o){return o||{};},fly=function(dom){flyEl.dom=dom;flyEl.id=Ext.id(dom);return flyEl;},getQueue=function(id){if(!queues[id]){queues[id]=[];} +return queues[id];},setQueue=function(id,value){queues[id]=value;};Ext.enableFx=TRUE;Ext.Fx={switchStatements:function(key,fn,argHash){return fn.apply(this,argHash[key]);},slideIn:function(anchor,o){o=getObject(o);var me=this,dom=me.dom,st=dom.style,xy,r,b,wrap,after,st,args,pt,bw,bh;anchor=anchor||"t";me.queueFx(o,function(){xy=fly(dom).getXY();fly(dom).fixDisplay();r=fly(dom).getFxRestore();b={x:xy[0],y:xy[1],0:xy[0],1:xy[1],width:dom.offsetWidth,height:dom.offsetHeight};b.right=b.x+b.width;b.bottom=b.y+b.height;fly(dom).setWidth(b.width).setHeight(b.height);wrap=fly(dom).fxWrap(r.pos,o,HIDDEN);st.visibility=VISIBLE;st.position=ABSOLUTE;function after(){fly(dom).fxUnwrap(wrap,r.pos,o);st.width=r.width;st.height=r.height;fly(dom).afterFx(o);} +pt={to:[b.x,b.y]};bw={to:b.width};bh={to:b.height};function argCalc(wrap,style,ww,wh,sXY,sXYval,s1,s2,w,h,p){var ret={};fly(wrap).setWidth(ww).setHeight(wh);if(fly(wrap)[sXY]){fly(wrap)[sXY](sXYval);} +style[s1]=style[s2]="0";if(w){ret.width=w;} +if(h){ret.height=h;} +if(p){ret.points=p;} +return ret;};args=fly(dom).switchStatements(anchor.toLowerCase(),argCalc,{t:[wrap,st,b.width,0,NULL,NULL,LEFT,BOTTOM,NULL,bh,NULL],l:[wrap,st,0,b.height,NULL,NULL,RIGHT,TOP,bw,NULL,NULL],r:[wrap,st,b.width,b.height,SETX,b.right,LEFT,TOP,NULL,NULL,pt],b:[wrap,st,b.width,b.height,SETY,b.bottom,LEFT,TOP,NULL,bh,pt],tl:[wrap,st,0,0,NULL,NULL,RIGHT,BOTTOM,bw,bh,pt],bl:[wrap,st,0,0,SETY,b.y+b.height,RIGHT,TOP,bw,bh,pt],br:[wrap,st,0,0,SETXY,[b.right,b.bottom],LEFT,TOP,bw,bh,pt],tr:[wrap,st,0,0,SETX,b.x+b.width,LEFT,BOTTOM,bw,bh,pt]});st.visibility=VISIBLE;fly(wrap).show();arguments.callee.anim=fly(wrap).fxanim(args,o,MOTION,.5,EASEOUT,after);});return me;},slideOut:function(anchor,o){o=getObject(o);var me=this,dom=me.dom,st=dom.style,xy=me.getXY(),wrap,r,b,a,zero={to:0};anchor=anchor||"t";me.queueFx(o,function(){r=fly(dom).getFxRestore();b={x:xy[0],y:xy[1],0:xy[0],1:xy[1],width:dom.offsetWidth,height:dom.offsetHeight};b.right=b.x+b.width;b.bottom=b.y+b.height;fly(dom).setWidth(b.width).setHeight(b.height);wrap=fly(dom).fxWrap(r.pos,o,VISIBLE);st.visibility=VISIBLE;st.position=ABSOLUTE;fly(wrap).setWidth(b.width).setHeight(b.height);function after(){o.useDisplay?fly(dom).setDisplayed(FALSE):fly(dom).hide();fly(dom).fxUnwrap(wrap,r.pos,o);st.width=r.width;st.height=r.height;fly(dom).afterFx(o);} +function argCalc(style,s1,s2,p1,v1,p2,v2,p3,v3){var ret={};style[s1]=style[s2]="0";ret[p1]=v1;if(p2){ret[p2]=v2;} +if(p3){ret[p3]=v3;} +return ret;};a=fly(dom).switchStatements(anchor.toLowerCase(),argCalc,{t:[st,LEFT,BOTTOM,HEIGHT,zero],l:[st,RIGHT,TOP,WIDTH,zero],r:[st,LEFT,TOP,WIDTH,zero,POINTS,{to:[b.right,b.y]}],b:[st,LEFT,TOP,HEIGHT,zero,POINTS,{to:[b.x,b.bottom]}],tl:[st,RIGHT,BOTTOM,WIDTH,zero,HEIGHT,zero],bl:[st,RIGHT,TOP,WIDTH,zero,HEIGHT,zero,POINTS,{to:[b.x,b.bottom]}],br:[st,LEFT,TOP,WIDTH,zero,HEIGHT,zero,POINTS,{to:[b.x+b.width,b.bottom]}],tr:[st,LEFT,BOTTOM,WIDTH,zero,HEIGHT,zero,POINTS,{to:[b.right,b.y]}]});arguments.callee.anim=fly(wrap).fxanim(a,o,MOTION,.5,EASEOUT,after);});return me;},puff:function(o){o=getObject(o);var me=this,dom=me.dom,st=dom.style,width,height,r;me.queueFx(o,function(){width=fly(dom).getWidth();height=fly(dom).getHeight();fly(dom).clearOpacity();fly(dom).show();r=fly(dom).getFxRestore();function after(){o.useDisplay?fly(dom).setDisplayed(FALSE):fly(dom).hide();fly(dom).clearOpacity();fly(dom).setPositioning(r.pos);st.width=r.width;st.height=r.height;st.fontSize='';fly(dom).afterFx(o);} +arguments.callee.anim=fly(dom).fxanim({width:{to:fly(dom).adjustWidth(width*2)},height:{to:fly(dom).adjustHeight(height*2)},points:{by:[-width*.5,-height*.5]},opacity:{to:0},fontSize:{to:200,unit:"%"}},o,MOTION,.5,EASEOUT,after);});return me;},switchOff:function(o){o=getObject(o);var me=this,dom=me.dom,st=dom.style,r;me.queueFx(o,function(){fly(dom).clearOpacity();fly(dom).clip();r=fly(dom).getFxRestore();function after(){o.useDisplay?fly(dom).setDisplayed(FALSE):fly(dom).hide();fly(dom).clearOpacity();fly(dom).setPositioning(r.pos);st.width=r.width;st.height=r.height;fly(dom).afterFx(o);};fly(dom).fxanim({opacity:{to:0.3}},NULL,NULL,.1,NULL,function(){fly(dom).clearOpacity();(function(){fly(dom).fxanim({height:{to:1},points:{by:[0,fly(dom).getHeight()*.5]}},o,MOTION,0.3,'easeIn',after);}).defer(100);});});return me;},highlight:function(color,o){o=getObject(o);var me=this,dom=me.dom,attr=o.attr||"backgroundColor",a={},restore;me.queueFx(o,function(){fly(dom).clearOpacity();fly(dom).show();function after(){dom.style[attr]=restore;fly(dom).afterFx(o);} +restore=dom.style[attr];a[attr]={from:color||"ffff9c",to:o.endColor||fly(dom).getColor(attr)||"ffffff"};arguments.callee.anim=fly(dom).fxanim(a,o,'color',1,'easeIn',after);});return me;},frame:function(color,count,o){o=getObject(o);var me=this,dom=me.dom,proxy,active;me.queueFx(o,function(){color=color||'#C3DAF9';if(color.length==6){color='#'+color;} +count=count||1;fly(dom).show();var xy=fly(dom).getXY(),b={x:xy[0],y:xy[1],0:xy[0],1:xy[1],width:dom.offsetWidth,height:dom.offsetHeight},queue=function(){proxy=fly(document.body||document.documentElement).createChild({style:{position:ABSOLUTE,'z-index':35000,border:'0px solid '+color}});return proxy.queueFx({},animFn);};arguments.callee.anim={isAnimated:true,stop:function(){count=0;proxy.stopFx();}};function animFn(){var scale=Ext.isBorderBox?2:1;active=proxy.anim({top:{from:b.y,to:b.y-20},left:{from:b.x,to:b.x-20},borderWidth:{from:0,to:10},opacity:{from:1,to:0},height:{from:b.height,to:b.height+20*scale},width:{from:b.width,to:b.width+20*scale}},{duration:o.duration||1,callback:function(){proxy.remove();--count>0?queue():fly(dom).afterFx(o);}});arguments.callee.anim={isAnimated:true,stop:function(){active.stop();}};};queue();});return me;},pause:function(seconds){var dom=this.dom,t;this.queueFx({},function(){t=setTimeout(function(){fly(dom).afterFx({});},seconds*1000);arguments.callee.anim={isAnimated:true,stop:function(){clearTimeout(t);fly(dom).afterFx({});}};});return this;},fadeIn:function(o){o=getObject(o);var me=this,dom=me.dom,to=o.endOpacity||1;me.queueFx(o,function(){fly(dom).setOpacity(0);fly(dom).fixDisplay();dom.style.visibility=VISIBLE;arguments.callee.anim=fly(dom).fxanim({opacity:{to:to}},o,NULL,.5,EASEOUT,function(){if(to==1){fly(dom).clearOpacity();} +fly(dom).afterFx(o);});});return me;},fadeOut:function(o){o=getObject(o);var me=this,dom=me.dom,style=dom.style,to=o.endOpacity||0;me.queueFx(o,function(){arguments.callee.anim=fly(dom).fxanim({opacity:{to:to}},o,NULL,.5,EASEOUT,function(){if(to==0){Ext.Element.data(dom,'visibilityMode')==Ext.Element.DISPLAY||o.useDisplay?style.display="none":style.visibility=HIDDEN;fly(dom).clearOpacity();} +fly(dom).afterFx(o);});});return me;},scale:function(w,h,o){this.shift(Ext.apply({},o,{width:w,height:h}));return this;},shift:function(o){o=getObject(o);var dom=this.dom,a={};this.queueFx(o,function(){for(var prop in o){if(o[prop]!=UNDEFINED){a[prop]={to:o[prop]};}} +a.width?a.width.to=fly(dom).adjustWidth(o.width):a;a.height?a.height.to=fly(dom).adjustWidth(o.height):a;if(a.x||a.y||a.xy){a.points=a.xy||{to:[a.x?a.x.to:fly(dom).getX(),a.y?a.y.to:fly(dom).getY()]};} +arguments.callee.anim=fly(dom).fxanim(a,o,MOTION,.35,EASEOUT,function(){fly(dom).afterFx(o);});});return this;},ghost:function(anchor,o){o=getObject(o);var me=this,dom=me.dom,st=dom.style,a={opacity:{to:0},points:{}},pt=a.points,r,w,h;anchor=anchor||"b";me.queueFx(o,function(){r=fly(dom).getFxRestore();w=fly(dom).getWidth();h=fly(dom).getHeight();function after(){o.useDisplay?fly(dom).setDisplayed(FALSE):fly(dom).hide();fly(dom).clearOpacity();fly(dom).setPositioning(r.pos);st.width=r.width;st.height=r.height;fly(dom).afterFx(o);} +pt.by=fly(dom).switchStatements(anchor.toLowerCase(),function(v1,v2){return[v1,v2];},{t:[0,-h],l:[-w,0],r:[w,0],b:[0,h],tl:[-w,-h],bl:[-w,h],br:[w,h],tr:[w,-h]});arguments.callee.anim=fly(dom).fxanim(a,o,MOTION,.5,EASEOUT,after);});return me;},syncFx:function(){var me=this;me.fxDefaults=Ext.apply(me.fxDefaults||{},{block:FALSE,concurrent:TRUE,stopFx:FALSE});return me;},sequenceFx:function(){var me=this;me.fxDefaults=Ext.apply(me.fxDefaults||{},{block:FALSE,concurrent:FALSE,stopFx:FALSE});return me;},nextFx:function(){var ef=getQueue(this.dom.id)[0];if(ef){ef.call(this);}},hasActiveFx:function(){return getQueue(this.dom.id)[0];},stopFx:function(finish){var me=this,id=me.dom.id;if(me.hasActiveFx()){var cur=getQueue(id)[0];if(cur&&cur.anim){if(cur.anim.isAnimated){setQueue(id,[cur]);cur.anim.stop(finish!==undefined?finish:TRUE);}else{setQueue(id,[]);}}} +return me;},beforeFx:function(o){if(this.hasActiveFx()&&!o.concurrent){if(o.stopFx){this.stopFx();return TRUE;} +return FALSE;} +return TRUE;},hasFxBlock:function(){var q=getQueue(this.dom.id);return q&&q[0]&&q[0].block;},queueFx:function(o,fn){var me=fly(this.dom);if(!me.hasFxBlock()){Ext.applyIf(o,me.fxDefaults);if(!o.concurrent){var run=me.beforeFx(o);fn.block=o.block;getQueue(me.dom.id).push(fn);if(run){me.nextFx();}}else{fn.call(me);}} +return me;},fxWrap:function(pos,o,vis){var dom=this.dom,wrap,wrapXY;if(!o.wrap||!(wrap=Ext.getDom(o.wrap))){if(o.fixPosition){wrapXY=fly(dom).getXY();} +var div=document.createElement("div");div.style.visibility=vis;wrap=dom.parentNode.insertBefore(div,dom);fly(wrap).setPositioning(pos);if(fly(wrap).isStyle(POSITION,"static")){fly(wrap).position("relative");} +fly(dom).clearPositioning('auto');fly(wrap).clip();wrap.appendChild(dom);if(wrapXY){fly(wrap).setXY(wrapXY);}} +return wrap;},fxUnwrap:function(wrap,pos,o){var dom=this.dom;fly(dom).clearPositioning();fly(dom).setPositioning(pos);if(!o.wrap){var pn=fly(wrap).dom.parentNode;pn.insertBefore(dom,wrap);fly(wrap).remove();}},getFxRestore:function(){var st=this.dom.style;return{pos:this.getPositioning(),width:st.width,height:st.height};},afterFx:function(o){var dom=this.dom,id=dom.id;if(o.afterStyle){fly(dom).setStyle(o.afterStyle);} +if(o.afterCls){fly(dom).addClass(o.afterCls);} +if(o.remove==TRUE){fly(dom).remove();} +if(o.callback){o.callback.call(o.scope,fly(dom));} +if(!o.concurrent){getQueue(id).shift();fly(dom).nextFx();}},fxanim:function(args,opt,animType,defaultDur,defaultEase,cb){animType=animType||'run';opt=opt||{};var anim=Ext.lib.Anim[animType](this.dom,args,(opt.duration||defaultDur)||.35,(opt.easing||defaultEase)||EASEOUT,cb,this);opt.anim=anim;return anim;}};Ext.Fx.resize=Ext.Fx.scale;Ext.Element.addMethods(Ext.Fx);})();Ext.CompositeElementLite=function(els,root){this.elements=[];this.add(els,root);this.el=new Ext.Element.Flyweight();};Ext.CompositeElementLite.prototype={isComposite:true,getElement:function(el){var e=this.el;e.dom=el;e.id=el.id;return e;},transformElement:function(el){return Ext.getDom(el);},getCount:function(){return this.elements.length;},add:function(els,root){var me=this,elements=me.elements;if(!els){return this;} +if(typeof els=="string"){els=Ext.Element.selectorFunction(els,root);}else if(els.isComposite){els=els.elements;}else if(!Ext.isIterable(els)){els=[els];} +for(var i=0,len=els.length;i<len;++i){elements.push(me.transformElement(els[i]));} +return me;},invoke:function(fn,args){var me=this,els=me.elements,len=els.length,e,i;for(i=0;i<len;i++){e=els[i];if(e){Ext.Element.prototype[fn].apply(me.getElement(e),args);}} +return me;},item:function(index){var me=this,el=me.elements[index],out=null;if(el){out=me.getElement(el);} +return out;},addListener:function(eventName,handler,scope,opt){var els=this.elements,len=els.length,i,e;for(i=0;i<len;i++){e=els[i];if(e){Ext.EventManager.on(e,eventName,handler,scope||e,opt);}} +return this;},each:function(fn,scope){var me=this,els=me.elements,len=els.length,i,e;for(i=0;i<len;i++){e=els[i];if(e){e=this.getElement(e);if(fn.call(scope||e,e,me,i)===false){break;}}} +return me;},fill:function(els){var me=this;me.elements=[];me.add(els);return me;},filter:function(selector){var els=[],me=this,fn=Ext.isFunction(selector)?selector:function(el){return el.is(selector);};me.each(function(el,self,i){if(fn(el,i)!==false){els[els.length]=me.transformElement(el);}});me.elements=els;return me;},indexOf:function(el){return this.elements.indexOf(this.transformElement(el));},replaceElement:function(el,replacement,domReplace){var index=!isNaN(el)?el:this.indexOf(el),d;if(index>-1){replacement=Ext.getDom(replacement);if(domReplace){d=this.elements[index];d.parentNode.insertBefore(replacement,d);Ext.removeNode(d);} +this.elements.splice(index,1,replacement);} +return this;},clear:function(){this.elements=[];}};Ext.CompositeElementLite.prototype.on=Ext.CompositeElementLite.prototype.addListener;Ext.CompositeElementLite.importElementMethods=function(){var fnName,ElProto=Ext.Element.prototype,CelProto=Ext.CompositeElementLite.prototype;for(fnName in ElProto){if(typeof ElProto[fnName]=='function'){(function(fnName){CelProto[fnName]=CelProto[fnName]||function(){return this.invoke(fnName,arguments);};}).call(CelProto,fnName);}}};Ext.CompositeElementLite.importElementMethods();if(Ext.DomQuery){Ext.Element.selectorFunction=Ext.DomQuery.select;} +Ext.Element.select=function(selector,root){var els;if(typeof selector=="string"){els=Ext.Element.selectorFunction(selector,root);}else if(selector.length!==undefined){els=selector;}else{throw"Invalid selector";} +return new Ext.CompositeElementLite(els);};Ext.select=Ext.Element.select;(function(){var BEFOREREQUEST="beforerequest",REQUESTCOMPLETE="requestcomplete",REQUESTEXCEPTION="requestexception",UNDEFINED=undefined,LOAD='load',POST='POST',GET='GET',WINDOW=window;Ext.data.Connection=function(config){Ext.apply(this,config);this.addEvents(BEFOREREQUEST,REQUESTCOMPLETE,REQUESTEXCEPTION);Ext.data.Connection.superclass.constructor.call(this);};Ext.extend(Ext.data.Connection,Ext.util.Observable,{timeout:30000,autoAbort:false,disableCaching:true,disableCachingParam:'_dc',request:function(o){var me=this;if(me.fireEvent(BEFOREREQUEST,me,o)){if(o.el){if(!Ext.isEmpty(o.indicatorText)){me.indicatorText='<div class="loading-indicator">'+o.indicatorText+"</div>";} +if(me.indicatorText){Ext.getDom(o.el).innerHTML=me.indicatorText;} +o.success=(Ext.isFunction(o.success)?o.success:function(){}).createInterceptor(function(response){Ext.getDom(o.el).innerHTML=response.responseText;});} +var p=o.params,url=o.url||me.url,method,cb={success:me.handleResponse,failure:me.handleFailure,scope:me,argument:{options:o},timeout:Ext.num(o.timeout,me.timeout)},form,serForm;if(Ext.isFunction(p)){p=p.call(o.scope||WINDOW,o);} +p=Ext.urlEncode(me.extraParams,Ext.isObject(p)?Ext.urlEncode(p):p);if(Ext.isFunction(url)){url=url.call(o.scope||WINDOW,o);} +if((form=Ext.getDom(o.form))){url=url||form.action;if(o.isUpload||(/multipart\/form-data/i.test(form.getAttribute("enctype")))){return me.doFormUpload.call(me,o,p,url);} +serForm=Ext.lib.Ajax.serializeForm(form);p=p?(p+'&'+serForm):serForm;} +method=o.method||me.method||((p||o.xmlData||o.jsonData)?POST:GET);if(method===GET&&(me.disableCaching&&o.disableCaching!==false)||o.disableCaching===true){var dcp=o.disableCachingParam||me.disableCachingParam;url=Ext.urlAppend(url,dcp+'='+(new Date().getTime()));} +o.headers=Ext.apply(o.headers||{},me.defaultHeaders||{});if(o.autoAbort===true||me.autoAbort){me.abort();} +if((method==GET||o.xmlData||o.jsonData)&&p){url=Ext.urlAppend(url,p);p='';} +return(me.transId=Ext.lib.Ajax.request(method,url,cb,p,o));}else{return o.callback?o.callback.apply(o.scope,[o,UNDEFINED,UNDEFINED]):null;}},isLoading:function(transId){return transId?Ext.lib.Ajax.isCallInProgress(transId):!!this.transId;},abort:function(transId){if(transId||this.isLoading()){Ext.lib.Ajax.abort(transId||this.transId);}},handleResponse:function(response){this.transId=false;var options=response.argument.options;response.argument=options?options.argument:null;this.fireEvent(REQUESTCOMPLETE,this,response,options);if(options.success){options.success.call(options.scope,response,options);} +if(options.callback){options.callback.call(options.scope,options,true,response);}},handleFailure:function(response,e){this.transId=false;var options=response.argument.options;response.argument=options?options.argument:null;this.fireEvent(REQUESTEXCEPTION,this,response,options,e);if(options.failure){options.failure.call(options.scope,response,options);} +if(options.callback){options.callback.call(options.scope,options,false,response);}},doFormUpload:function(o,ps,url){var id=Ext.id(),doc=document,frame=doc.createElement('iframe'),form=Ext.getDom(o.form),hiddens=[],hd,encoding='multipart/form-data',buf={target:form.target,method:form.method,encoding:form.encoding,enctype:form.enctype,action:form.action};Ext.fly(frame).set({id:id,name:id,cls:'x-hidden',src:Ext.SSL_SECURE_URL});doc.body.appendChild(frame);if(Ext.isIE){document.frames[id].name=id;} +Ext.fly(form).set({target:id,method:POST,enctype:encoding,encoding:encoding,action:url||buf.action});Ext.iterate(Ext.urlDecode(ps,false),function(k,v){hd=doc.createElement('input');Ext.fly(hd).set({type:'hidden',value:v,name:k});form.appendChild(hd);hiddens.push(hd);});function cb(){var me=this,r={responseText:'',responseXML:null,argument:o.argument},doc,firstChild;try{doc=frame.contentWindow.document||frame.contentDocument||WINDOW.frames[id].document;if(doc){if(doc.body){if(/textarea/i.test((firstChild=doc.body.firstChild||{}).tagName)){r.responseText=firstChild.value;}else{r.responseText=doc.body.innerHTML;}} +r.responseXML=doc.XMLDocument||doc;}} +catch(e){} +Ext.EventManager.removeListener(frame,LOAD,cb,me);me.fireEvent(REQUESTCOMPLETE,me,r,o);function runCallback(fn,scope,args){if(Ext.isFunction(fn)){fn.apply(scope,args);}} +runCallback(o.success,o.scope,[r,o]);runCallback(o.callback,o.scope,[o,true,r]);if(!me.debugUploads){setTimeout(function(){Ext.removeNode(frame);},100);}} +Ext.EventManager.on(frame,LOAD,cb,this);form.submit();Ext.fly(form).set(buf);Ext.each(hiddens,function(h){Ext.removeNode(h);});}});})();Ext.Ajax=new Ext.data.Connection({autoAbort:false,serializeForm:function(form){return Ext.lib.Ajax.serializeForm(form);}});Ext.util.JSON=new(function(){var useHasOwn=!!{}.hasOwnProperty,isNative=function(){var useNative=null;return function(){if(useNative===null){useNative=Ext.USE_NATIVE_JSON&&window.JSON&&JSON.toString()=='[object JSON]';} +return useNative;};}(),pad=function(n){return n<10?"0"+n:n;},doDecode=function(json){return eval("("+json+")");},doEncode=function(o){if(!Ext.isDefined(o)||o===null){return"null";}else if(Ext.isArray(o)){return encodeArray(o);}else if(Ext.isDate(o)){return Ext.util.JSON.encodeDate(o);}else if(Ext.isString(o)){return encodeString(o);}else if(typeof o=="number"){return isFinite(o)?String(o):"null";}else if(Ext.isBoolean(o)){return String(o);}else{var a=["{"],b,i,v;for(i in o){if(!o.getElementsByTagName){if(!useHasOwn||o.hasOwnProperty(i)){v=o[i];switch(typeof v){case"undefined":case"function":case"unknown":break;default:if(b){a.push(',');} +a.push(doEncode(i),":",v===null?"null":doEncode(v));b=true;}}}} +a.push("}");return a.join("");}},m={"\b":'\\b',"\t":'\\t',"\n":'\\n',"\f":'\\f',"\r":'\\r','"':'\\"',"\\":'\\\\'},encodeString=function(s){if(/["\\\x00-\x1f]/.test(s)){return'"'+s.replace(/([\x00-\x1f\\"])/g,function(a,b){var c=m[b];if(c){return c;} +c=b.charCodeAt();return"\\u00"+ +Math.floor(c/16).toString(16)+ +(c%16).toString(16);})+'"';} +return'"'+s+'"';},encodeArray=function(o){var a=["["],b,i,l=o.length,v;for(i=0;i<l;i+=1){v=o[i];switch(typeof v){case"undefined":case"function":case"unknown":break;default:if(b){a.push(',');} +a.push(v===null?"null":Ext.util.JSON.encode(v));b=true;}} +a.push("]");return a.join("");};this.encodeDate=function(o){return'"'+o.getFullYear()+"-"+ +pad(o.getMonth()+1)+"-"+ +pad(o.getDate())+"T"+ +pad(o.getHours())+":"+ +pad(o.getMinutes())+":"+ +pad(o.getSeconds())+'"';};this.encode=function(){var ec;return function(o){if(!ec){ec=isNative()?JSON.stringify:doEncode;} +return ec(o);};}();this.decode=function(){var dc;return function(json){if(!dc){dc=isNative()?JSON.parse:doDecode;} +return dc(json);};}();})();Ext.encode=Ext.util.JSON.encode;Ext.decode=Ext.util.JSON.decode;Ext.EventManager=function(){var docReadyEvent,docReadyProcId,docReadyState=false,DETECT_NATIVE=Ext.isGecko||Ext.isWebKit||Ext.isSafari,E=Ext.lib.Event,D=Ext.lib.Dom,DOC=document,WINDOW=window,DOMCONTENTLOADED="DOMContentLoaded",COMPLETE='complete',propRe=/^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/,specialElCache=[];function getId(el){var id=false,i=0,len=specialElCache.length,skip=false,o;if(el){if(el.getElementById||el.navigator){for(;i<len;++i){o=specialElCache[i];if(o.el===el){id=o.id;break;}} +if(!id){id=Ext.id(el);specialElCache.push({id:id,el:el});skip=true;}}else{id=Ext.id(el);} +if(!Ext.elCache[id]){Ext.Element.addToCache(new Ext.Element(el),id);if(skip){Ext.elCache[id].skipGC=true;}}} +return id;} +function addListener(el,ename,fn,task,wrap,scope){el=Ext.getDom(el);var id=getId(el),es=Ext.elCache[id].events,wfn;wfn=E.on(el,ename,wrap);es[ename]=es[ename]||[];es[ename].push([fn,wrap,scope,wfn,task]);if(el.addEventListener&&ename=="mousewheel"){var args=["DOMMouseScroll",wrap,false];el.addEventListener.apply(el,args);Ext.EventManager.addListener(WINDOW,'unload',function(){el.removeEventListener.apply(el,args);});} +if(el==DOC&&ename=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.addListener(wrap);}} +function doScrollChk(){if(window!=top){return false;} +try{DOC.documentElement.doScroll('left');}catch(e){return false;} +fireDocReady();return true;} +function checkReadyState(e){if(Ext.isIE&&doScrollChk()){return true;} +if(DOC.readyState==COMPLETE){fireDocReady();return true;} +docReadyState||(docReadyProcId=setTimeout(arguments.callee,2));return false;} +var styles;function checkStyleSheets(e){styles||(styles=Ext.query('style, link[rel=stylesheet]'));if(styles.length==DOC.styleSheets.length){fireDocReady();return true;} +docReadyState||(docReadyProcId=setTimeout(arguments.callee,2));return false;} +function OperaDOMContentLoaded(e){DOC.removeEventListener(DOMCONTENTLOADED,arguments.callee,false);checkStyleSheets();} +function fireDocReady(e){if(!docReadyState){docReadyState=true;if(docReadyProcId){clearTimeout(docReadyProcId);} +if(DETECT_NATIVE){DOC.removeEventListener(DOMCONTENTLOADED,fireDocReady,false);} +if(Ext.isIE&&checkReadyState.bindIE){DOC.detachEvent('onreadystatechange',checkReadyState);} +E.un(WINDOW,"load",arguments.callee);} +if(docReadyEvent&&!Ext.isReady){Ext.isReady=true;docReadyEvent.fire();docReadyEvent.listeners=[];}} +function initDocReady(){docReadyEvent||(docReadyEvent=new Ext.util.Event());if(DETECT_NATIVE){DOC.addEventListener(DOMCONTENTLOADED,fireDocReady,false);} +if(Ext.isIE){if(!checkReadyState()){checkReadyState.bindIE=true;DOC.attachEvent('onreadystatechange',checkReadyState);}}else if(Ext.isOpera){(DOC.readyState==COMPLETE&&checkStyleSheets())||DOC.addEventListener(DOMCONTENTLOADED,OperaDOMContentLoaded,false);}else if(Ext.isWebKit){checkReadyState();} +E.on(WINDOW,"load",fireDocReady);} +function createTargeted(h,o){return function(){var args=Ext.toArray(arguments);if(o.target==Ext.EventObject.setEvent(args[0]).target){h.apply(this,args);}};} +function createBuffered(h,o,task){return function(e){task.delay(o.buffer,h,null,[new Ext.EventObjectImpl(e)]);};} +function createSingle(h,el,ename,fn,scope){return function(e){Ext.EventManager.removeListener(el,ename,fn,scope);h(e);};} +function createDelayed(h,o,fn){return function(e){var task=new Ext.util.DelayedTask(h);if(!fn.tasks){fn.tasks=[];} +fn.tasks.push(task);task.delay(o.delay||10,h,null,[new Ext.EventObjectImpl(e)]);};} +function listen(element,ename,opt,fn,scope){var o=(!opt||typeof opt=="boolean")?{}:opt,el=Ext.getDom(element),task;fn=fn||o.fn;scope=scope||o.scope;if(!el){throw"Error listening for \""+ename+'\". Element "'+element+'" doesn\'t exist.';} +function h(e){if(!Ext){return;} +e=Ext.EventObject.setEvent(e);var t;if(o.delegate){if(!(t=e.getTarget(o.delegate,el))){return;}}else{t=e.target;} +if(o.stopEvent){e.stopEvent();} +if(o.preventDefault){e.preventDefault();} +if(o.stopPropagation){e.stopPropagation();} +if(o.normalized===false){e=e.browserEvent;} +fn.call(scope||el,e,t,o);} +if(o.target){h=createTargeted(h,o);} +if(o.delay){h=createDelayed(h,o,fn);} +if(o.single){h=createSingle(h,el,ename,fn,scope);} +if(o.buffer){task=new Ext.util.DelayedTask(h);h=createBuffered(h,o,task);} +addListener(el,ename,fn,task,h,scope);return h;} +var pub={addListener:function(element,eventName,fn,scope,options){if(typeof eventName=='object'){var o=eventName,e,val;for(e in o){val=o[e];if(!propRe.test(e)){if(Ext.isFunction(val)){listen(element,e,o,val,o.scope);}else{listen(element,e,val);}}}}else{listen(element,eventName,options,fn,scope);}},removeListener:function(el,eventName,fn,scope){el=Ext.getDom(el);var id=getId(el),f=el&&(Ext.elCache[id].events)[eventName]||[],wrap,i,l,k,len,fnc;for(i=0,len=f.length;i<len;i++){if(Ext.isArray(fnc=f[i])&&fnc[0]==fn&&(!scope||fnc[2]==scope)){if(fnc[4]){fnc[4].cancel();} +k=fn.tasks&&fn.tasks.length;if(k){while(k--){fn.tasks[k].cancel();} +delete fn.tasks;} +wrap=fnc[1];E.un(el,eventName,E.extAdapter?fnc[3]:wrap);if(wrap&&el.addEventListener&&eventName=="mousewheel"){el.removeEventListener("DOMMouseScroll",wrap,false);} +if(wrap&&el==DOC&&eventName=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.removeListener(wrap);} +f.splice(i,1);if(f.length===0){delete Ext.elCache[id].events[eventName];} +for(k in Ext.elCache[id].events){return false;} +Ext.elCache[id].events={};return false;}}},removeAll:function(el){el=Ext.getDom(el);var id=getId(el),ec=Ext.elCache[id]||{},es=ec.events||{},f,i,len,ename,fn,k,wrap;for(ename in es){if(es.hasOwnProperty(ename)){f=es[ename];for(i=0,len=f.length;i<len;i++){fn=f[i];if(fn[4]){fn[4].cancel();} +if(fn[0].tasks&&(k=fn[0].tasks.length)){while(k--){fn[0].tasks[k].cancel();} +delete fn.tasks;} +wrap=fn[1];E.un(el,ename,E.extAdapter?fn[3]:wrap);if(el.addEventListener&&wrap&&ename=="mousewheel"){el.removeEventListener("DOMMouseScroll",wrap,false);} +if(wrap&&el==DOC&&ename=="mousedown"){Ext.EventManager.stoppedMouseDownEvent.removeListener(wrap);}}}} +if(Ext.elCache[id]){Ext.elCache[id].events={};}},getListeners:function(el,eventName){el=Ext.getDom(el);var id=getId(el),ec=Ext.elCache[id]||{},es=ec.events||{},results=[];if(es&&es[eventName]){return es[eventName];}else{return null;}},purgeElement:function(el,recurse,eventName){el=Ext.getDom(el);var id=getId(el),ec=Ext.elCache[id]||{},es=ec.events||{},i,f,len;if(eventName){if(es&&es.hasOwnProperty(eventName)){f=es[eventName];for(i=0,len=f.length;i<len;i++){Ext.EventManager.removeListener(el,eventName,f[i][0]);}}}else{Ext.EventManager.removeAll(el);} +if(recurse&&el&&el.childNodes){for(i=0,len=el.childNodes.length;i<len;i++){Ext.EventManager.purgeElement(el.childNodes[i],recurse,eventName);}}},_unload:function(){var el;for(el in Ext.elCache){Ext.EventManager.removeAll(el);} +delete Ext.elCache;delete Ext.Element._flyweights;var c,conn,tid,ajax=Ext.lib.Ajax;(typeof ajax.conn=='object')?conn=ajax.conn:conn={};for(tid in conn){c=conn[tid];if(c){ajax.abort({conn:c,tId:tid});}}},onDocumentReady:function(fn,scope,options){if(Ext.isReady){docReadyEvent||(docReadyEvent=new Ext.util.Event());docReadyEvent.addListener(fn,scope,options);docReadyEvent.fire();docReadyEvent.listeners=[];}else{if(!docReadyEvent){initDocReady();} +options=options||{};options.delay=options.delay||1;docReadyEvent.addListener(fn,scope,options);}},fireDocReady:fireDocReady};pub.on=pub.addListener;pub.un=pub.removeListener;pub.stoppedMouseDownEvent=new Ext.util.Event();return pub;}();Ext.onReady=Ext.EventManager.onDocumentReady;(function(){var initExtCss=function(){var bd=document.body||document.getElementsByTagName('body')[0];if(!bd){return false;} +var cls=[' ',Ext.isIE?"ext-ie "+(Ext.isIE6?'ext-ie6':(Ext.isIE7?'ext-ie7':'ext-ie8')):Ext.isGecko?"ext-gecko "+(Ext.isGecko2?'ext-gecko2':'ext-gecko3'):Ext.isOpera?"ext-opera":Ext.isWebKit?"ext-webkit":""];if(Ext.isSafari){cls.push("ext-safari "+(Ext.isSafari2?'ext-safari2':(Ext.isSafari3?'ext-safari3':'ext-safari4')));}else if(Ext.isChrome){cls.push("ext-chrome");} +if(Ext.isMac){cls.push("ext-mac");} +if(Ext.isLinux){cls.push("ext-linux");} +if(Ext.isStrict||Ext.isBorderBox){var p=bd.parentNode;if(p){Ext.fly(p,'_internal').addClass(((Ext.isStrict&&Ext.isIE)||(!Ext.enableForcedBoxModel&&!Ext.isIE))?' ext-strict':' ext-border-box');}} +if(Ext.enableForcedBoxModel&&!Ext.isIE){Ext.isForcedBorderBox=true;cls.push("ext-forced-border-box");} +Ext.fly(bd,'_internal').addClass(cls);return true;};if(!initExtCss()){Ext.onReady(initExtCss);}})();(function(){var supports=Ext.apply(Ext.supports,{correctRightMargin:true,correctTransparentColor:true,cssFloat:true});var supportTests=function(){var div=document.createElement('div'),doc=document,view,last;div.innerHTML='<div style="height:30px;width:50px;"><div style="height:20px;width:20px;"></div></div><div style="float:left;background-color:transparent;">';doc.body.appendChild(div);last=div.lastChild;if((view=doc.defaultView)){if(view.getComputedStyle(div.firstChild.firstChild,null).marginRight!='0px'){supports.correctRightMargin=false;} +if(view.getComputedStyle(last,null).backgroundColor!='transparent'){supports.correctTransparentColor=false;}} +supports.cssFloat=!!last.style.cssFloat;doc.body.removeChild(div);};if(Ext.isReady){supportTests();}else{Ext.onReady(supportTests);}})();Ext.EventObject=function(){var E=Ext.lib.Event,clickRe=/(dbl)?click/,safariKeys={3:13,63234:37,63235:39,63232:38,63233:40,63276:33,63277:34,63272:46,63273:36,63275:35},btnMap=Ext.isIE?{1:0,4:1,2:2}:{0:0,1:1,2:2};Ext.EventObjectImpl=function(e){if(e){this.setEvent(e.browserEvent||e);}};Ext.EventObjectImpl.prototype={setEvent:function(e){var me=this;if(e==me||(e&&e.browserEvent)){return e;} +me.browserEvent=e;if(e){me.button=e.button?btnMap[e.button]:(e.which?e.which-1:-1);if(clickRe.test(e.type)&&me.button==-1){me.button=0;} +me.type=e.type;me.shiftKey=e.shiftKey;me.ctrlKey=e.ctrlKey||e.metaKey||false;me.altKey=e.altKey;me.keyCode=e.keyCode;me.charCode=e.charCode;me.target=E.getTarget(e);me.xy=E.getXY(e);}else{me.button=-1;me.shiftKey=false;me.ctrlKey=false;me.altKey=false;me.keyCode=0;me.charCode=0;me.target=null;me.xy=[0,0];} +return me;},stopEvent:function(){var me=this;if(me.browserEvent){if(me.browserEvent.type=='mousedown'){Ext.EventManager.stoppedMouseDownEvent.fire(me);} +E.stopEvent(me.browserEvent);}},preventDefault:function(){if(this.browserEvent){E.preventDefault(this.browserEvent);}},stopPropagation:function(){var me=this;if(me.browserEvent){if(me.browserEvent.type=='mousedown'){Ext.EventManager.stoppedMouseDownEvent.fire(me);} +E.stopPropagation(me.browserEvent);}},getCharCode:function(){return this.charCode||this.keyCode;},getKey:function(){return this.normalizeKey(this.keyCode||this.charCode);},normalizeKey:function(k){return Ext.isSafari?(safariKeys[k]||k):k;},getPageX:function(){return this.xy[0];},getPageY:function(){return this.xy[1];},getXY:function(){return this.xy;},getTarget:function(selector,maxDepth,returnEl){return selector?Ext.fly(this.target).findParent(selector,maxDepth,returnEl):(returnEl?Ext.get(this.target):this.target);},getRelatedTarget:function(){return this.browserEvent?E.getRelatedTarget(this.browserEvent):null;},getWheelDelta:function(){var e=this.browserEvent;var delta=0;if(e.wheelDelta){delta=e.wheelDelta/120;}else if(e.detail){delta=-e.detail/3;} +return delta;},within:function(el,related,allowEl){if(el){var t=this[related?"getRelatedTarget":"getTarget"]();return t&&((allowEl?(t==Ext.getDom(el)):false)||Ext.fly(el).contains(t));} +return false;}};return new Ext.EventObjectImpl();}();Ext.Loader=Ext.apply({},{load:function(fileList,callback,scope,preserveOrder){var scope=scope||this,head=document.getElementsByTagName("head")[0],fragment=document.createDocumentFragment(),numFiles=fileList.length,loadedFiles=0,me=this;var loadFileIndex=function(index){head.appendChild(me.buildScriptTag(fileList[index],onFileLoaded));};var onFileLoaded=function(){loadedFiles++;if(numFiles==loadedFiles&&typeof callback=='function'){callback.call(scope);}else{if(preserveOrder===true){loadFileIndex(loadedFiles);}}};if(preserveOrder===true){loadFileIndex.call(this,0);}else{Ext.each(fileList,function(file,index){fragment.appendChild(this.buildScriptTag(file,onFileLoaded));},this);head.appendChild(fragment);}},buildScriptTag:function(filename,callback){var script=document.createElement('script');script.type="text/javascript";script.src=filename;if(script.readyState){script.onreadystatechange=function(){if(script.readyState=="loaded"||script.readyState=="complete"){script.onreadystatechange=null;callback();}};}else{script.onload=callback;} +return script;}});Ext.ns("Ext.grid","Ext.list","Ext.dd","Ext.tree","Ext.form","Ext.menu","Ext.state","Ext.layout","Ext.app","Ext.ux","Ext.chart","Ext.direct");Ext.apply(Ext,function(){var E=Ext,idSeed=0,scrollWidth=null;return{emptyFn:function(){},BLANK_IMAGE_URL:Ext.isIE6||Ext.isIE7||Ext.isAir?'http:/'+'/www.extjs.com/s.gif':'data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',extendX:function(supr,fn){return Ext.extend(supr,fn(supr.prototype));},getDoc:function(){return Ext.get(document);},num:function(v,defaultValue){v=Number(Ext.isEmpty(v)||Ext.isArray(v)||typeof v=='boolean'||(typeof v=='string'&&v.trim().length==0)?NaN:v);return isNaN(v)?defaultValue:v;},value:function(v,defaultValue,allowBlank){return Ext.isEmpty(v,allowBlank)?defaultValue:v;},escapeRe:function(s){return s.replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},sequence:function(o,name,fn,scope){o[name]=o[name].createSequence(fn,scope);},addBehaviors:function(o){if(!Ext.isReady){Ext.onReady(function(){Ext.addBehaviors(o);});}else{var cache={},parts,b,s;for(b in o){if((parts=b.split('@'))[1]){s=parts[0];if(!cache[s]){cache[s]=Ext.select(s);} +cache[s].on(parts[1],o[b]);}} +cache=null;}},getScrollBarWidth:function(force){if(!Ext.isReady){return 0;} +if(force===true||scrollWidth===null){var div=Ext.getBody().createChild('<div class="x-hide-offsets" style="width:100px;height:50px;overflow:hidden;"><div style="height:200px;"></div></div>'),child=div.child('div',true);var w1=child.offsetWidth;div.setStyle('overflow',(Ext.isWebKit||Ext.isGecko)?'auto':'scroll');var w2=child.offsetWidth;div.remove();scrollWidth=w1-w2+2;} +return scrollWidth;},combine:function(){var as=arguments,l=as.length,r=[];for(var i=0;i<l;i++){var a=as[i];if(Ext.isArray(a)){r=r.concat(a);}else if(a.length!==undefined&&!a.substr){r=r.concat(Array.prototype.slice.call(a,0));}else{r.push(a);}} +return r;},copyTo:function(dest,source,names){if(typeof names=='string'){names=names.split(/[,;\s]/);} +Ext.each(names,function(name){if(source.hasOwnProperty(name)){dest[name]=source[name];}},this);return dest;},destroy:function(){Ext.each(arguments,function(arg){if(arg){if(Ext.isArray(arg)){this.destroy.apply(this,arg);}else if(typeof arg.destroy=='function'){arg.destroy();}else if(arg.dom){arg.remove();}}},this);},destroyMembers:function(o,arg1,arg2,etc){for(var i=1,a=arguments,len=a.length;i<len;i++){Ext.destroy(o[a[i]]);delete o[a[i]];}},clean:function(arr){var ret=[];Ext.each(arr,function(v){if(!!v){ret.push(v);}});return ret;},unique:function(arr){var ret=[],collect={};Ext.each(arr,function(v){if(!collect[v]){ret.push(v);} +collect[v]=true;});return ret;},flatten:function(arr){var worker=[];function rFlatten(a){Ext.each(a,function(v){if(Ext.isArray(v)){rFlatten(v);}else{worker.push(v);}});return worker;} +return rFlatten(arr);},min:function(arr,comp){var ret=arr[0];comp=comp||function(a,b){return a<b?-1:1;};Ext.each(arr,function(v){ret=comp(ret,v)==-1?ret:v;});return ret;},max:function(arr,comp){var ret=arr[0];comp=comp||function(a,b){return a>b?1:-1;};Ext.each(arr,function(v){ret=comp(ret,v)==1?ret:v;});return ret;},mean:function(arr){return arr.length>0?Ext.sum(arr)/arr.length:undefined;},sum:function(arr){var ret=0;Ext.each(arr,function(v){ret+=v;});return ret;},partition:function(arr,truth){var ret=[[],[]];Ext.each(arr,function(v,i,a){ret[(truth&&truth(v,i,a))||(!truth&&v)?0:1].push(v);});return ret;},invoke:function(arr,methodName){var ret=[],args=Array.prototype.slice.call(arguments,2);Ext.each(arr,function(v,i){if(v&&typeof v[methodName]=='function'){ret.push(v[methodName].apply(v,args));}else{ret.push(undefined);}});return ret;},pluck:function(arr,prop){var ret=[];Ext.each(arr,function(v){ret.push(v[prop]);});return ret;},zip:function(){var parts=Ext.partition(arguments,function(val){return typeof val!='function';}),arrs=parts[0],fn=parts[1][0],len=Ext.max(Ext.pluck(arrs,"length")),ret=[];for(var i=0;i<len;i++){ret[i]=[];if(fn){ret[i]=fn.apply(fn,Ext.pluck(arrs,i));}else{for(var j=0,aLen=arrs.length;j<aLen;j++){ret[i].push(arrs[j][i]);}}} +return ret;},getCmp:function(id){return Ext.ComponentMgr.get(id);},useShims:E.isIE6||(E.isMac&&E.isGecko2),type:function(o){if(o===undefined||o===null){return false;} +if(o.htmlElement){return'element';} +var t=typeof o;if(t=='object'&&o.nodeName){switch(o.nodeType){case 1:return'element';case 3:return(/\S/).test(o.nodeValue)?'textnode':'whitespace';}} +if(t=='object'||t=='function'){switch(o.constructor){case Array:return'array';case RegExp:return'regexp';case Date:return'date';} +if(typeof o.length=='number'&&typeof o.item=='function'){return'nodelist';}} +return t;},intercept:function(o,name,fn,scope){o[name]=o[name].createInterceptor(fn,scope);},callback:function(cb,scope,args,delay){if(typeof cb=='function'){if(delay){cb.defer(delay,scope,args||[]);}else{cb.apply(scope,args||[]);}}}};}());Ext.apply(Function.prototype,{createSequence:function(fcn,scope){var method=this;return(typeof fcn!='function')?this:function(){var retval=method.apply(this||window,arguments);fcn.apply(scope||this||window,arguments);return retval;};}});Ext.applyIf(String,{escape:function(string){return string.replace(/('|\\)/g,"\\$1");},leftPad:function(val,size,ch){var result=String(val);if(!ch){ch=" ";} +while(result.length<size){result=ch+result;} +return result;}});String.prototype.toggle=function(value,other){return this==value?other:value;};String.prototype.trim=function(){var re=/^\s+|\s+$/g;return function(){return this.replace(re,"");};}();Date.prototype.getElapsed=function(date){return Math.abs((date||new Date()).getTime()-this.getTime());};Ext.applyIf(Number.prototype,{constrain:function(min,max){return Math.min(Math.max(this,min),max);}});Ext.lib.Dom.getRegion=function(el){return Ext.lib.Region.getRegion(el);};Ext.lib.Region=function(t,r,b,l){var me=this;me.top=t;me[1]=t;me.right=r;me.bottom=b;me.left=l;me[0]=l;};Ext.lib.Region.prototype={contains:function(region){var me=this;return(region.left>=me.left&®ion.right<=me.right&®ion.top>=me.top&®ion.bottom<=me.bottom);},getArea:function(){var me=this;return((me.bottom-me.top)*(me.right-me.left));},intersect:function(region){var me=this,t=Math.max(me.top,region.top),r=Math.min(me.right,region.right),b=Math.min(me.bottom,region.bottom),l=Math.max(me.left,region.left);if(b>=t&&r>=l){return new Ext.lib.Region(t,r,b,l);}},union:function(region){var me=this,t=Math.min(me.top,region.top),r=Math.max(me.right,region.right),b=Math.max(me.bottom,region.bottom),l=Math.min(me.left,region.left);return new Ext.lib.Region(t,r,b,l);},constrainTo:function(r){var me=this;me.top=me.top.constrain(r.top,r.bottom);me.bottom=me.bottom.constrain(r.top,r.bottom);me.left=me.left.constrain(r.left,r.right);me.right=me.right.constrain(r.left,r.right);return me;},adjust:function(t,l,b,r){var me=this;me.top+=t;me.left+=l;me.right+=r;me.bottom+=b;return me;}};Ext.lib.Region.getRegion=function(el){var p=Ext.lib.Dom.getXY(el),t=p[1],r=p[0]+el.offsetWidth,b=p[1]+el.offsetHeight,l=p[0];return new Ext.lib.Region(t,r,b,l);};Ext.lib.Point=function(x,y){if(Ext.isArray(x)){y=x[1];x=x[0];} +var me=this;me.x=me.right=me.left=me[0]=x;me.y=me.top=me.bottom=me[1]=y;};Ext.lib.Point.prototype=new Ext.lib.Region();Ext.apply(Ext.DomHelper,function(){var pub,afterbegin='afterbegin',afterend='afterend',beforebegin='beforebegin',beforeend='beforeend',confRe=/tag|children|cn|html$/i;function doInsert(el,o,returnElement,pos,sibling,append){el=Ext.getDom(el);var newNode;if(pub.useDom){newNode=createDom(o,null);if(append){el.appendChild(newNode);}else{(sibling=='firstChild'?el:el.parentNode).insertBefore(newNode,el[sibling]||el);}}else{newNode=Ext.DomHelper.insertHtml(pos,el,Ext.DomHelper.createHtml(o));} +return returnElement?Ext.get(newNode,true):newNode;} +function createDom(o,parentNode){var el,doc=document,useSet,attr,val,cn;if(Ext.isArray(o)){el=doc.createDocumentFragment();for(var i=0,l=o.length;i<l;i++){createDom(o[i],el);}}else if(typeof o=='string'){el=doc.createTextNode(o);}else{el=doc.createElement(o.tag||'div');useSet=!!el.setAttribute;for(var attr in o){if(!confRe.test(attr)){val=o[attr];if(attr=='cls'){el.className=val;}else{if(useSet){el.setAttribute(attr,val);}else{el[attr]=val;}}}} +Ext.DomHelper.applyStyles(el,o.style);if((cn=o.children||o.cn)){createDom(cn,el);}else if(o.html){el.innerHTML=o.html;}} +if(parentNode){parentNode.appendChild(el);} +return el;} +pub={createTemplate:function(o){var html=Ext.DomHelper.createHtml(o);return new Ext.Template(html);},useDom:false,insertBefore:function(el,o,returnElement){return doInsert(el,o,returnElement,beforebegin);},insertAfter:function(el,o,returnElement){return doInsert(el,o,returnElement,afterend,'nextSibling');},insertFirst:function(el,o,returnElement){return doInsert(el,o,returnElement,afterbegin,'firstChild');},append:function(el,o,returnElement){return doInsert(el,o,returnElement,beforeend,'',true);},createDom:createDom};return pub;}());Ext.apply(Ext.Template.prototype,{disableFormats:false,re:/\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,argsRe:/^\s*['"](.*)["']\s*$/,compileARe:/\\/g,compileBRe:/(\r\n|\n)/g,compileCRe:/'/g,applyTemplate:function(values){var me=this,useF=me.disableFormats!==true,fm=Ext.util.Format,tpl=me;if(me.compiled){return me.compiled(values);} +function fn(m,name,format,args){if(format&&useF){if(format.substr(0,5)=="this."){return tpl.call(format.substr(5),values[name],values);}else{if(args){var re=me.argsRe;args=args.split(',');for(var i=0,len=args.length;i<len;i++){args[i]=args[i].replace(re,"$1");} +args=[values[name]].concat(args);}else{args=[values[name]];} +return fm[format].apply(fm,args);}}else{return values[name]!==undefined?values[name]:"";}} +return me.html.replace(me.re,fn);},compile:function(){var me=this,fm=Ext.util.Format,useF=me.disableFormats!==true,sep=Ext.isGecko?"+":",",body;function fn(m,name,format,args){if(format&&useF){args=args?','+args:"";if(format.substr(0,5)!="this."){format="fm."+format+'(';}else{format='this.call("'+format.substr(5)+'", ';args=", values";}}else{args='';format="(values['"+name+"'] == undefined ? '' : ";} +return"'"+sep+format+"values['"+name+"']"+args+")"+sep+"'";} +if(Ext.isGecko){body="this.compiled = function(values){ return '"+ +me.html.replace(me.compileARe,'\\\\').replace(me.compileBRe,'\\n').replace(me.compileCRe,"\\'").replace(me.re,fn)+"';};";}else{body=["this.compiled = function(values){ return ['"];body.push(me.html.replace(me.compileARe,'\\\\').replace(me.compileBRe,'\\n').replace(me.compileCRe,"\\'").replace(me.re,fn));body.push("'].join('');};");body=body.join('');} +eval(body);return me;},call:function(fnName,value,allValues){return this[fnName](value,allValues);}});Ext.Template.prototype.apply=Ext.Template.prototype.applyTemplate;Ext.util.Functions={createInterceptor:function(origFn,newFn,scope){var method=origFn;if(!Ext.isFunction(newFn)){return origFn;} +else{return function(){var me=this,args=arguments;newFn.target=me;newFn.method=origFn;return(newFn.apply(scope||me||window,args)!==false)?origFn.apply(me||window,args):null;};}},createDelegate:function(fn,obj,args,appendArgs){if(!Ext.isFunction(fn)){return fn;} +return function(){var callArgs=args||arguments;if(appendArgs===true){callArgs=Array.prototype.slice.call(arguments,0);callArgs=callArgs.concat(args);} +else if(Ext.isNumber(appendArgs)){callArgs=Array.prototype.slice.call(arguments,0);var applyArgs=[appendArgs,0].concat(args);Array.prototype.splice.apply(callArgs,applyArgs);} +return fn.apply(obj||window,callArgs);};},defer:function(fn,millis,obj,args,appendArgs){fn=Ext.util.Functions.createDelegate(fn,obj,args,appendArgs);if(millis>0){return setTimeout(fn,millis);} +fn();return 0;},createSequence:function(origFn,newFn,scope){if(!Ext.isFunction(newFn)){return origFn;} +else{return function(){var retval=origFn.apply(this||window,arguments);newFn.apply(scope||this||window,arguments);return retval;};}}};Ext.defer=Ext.util.Functions.defer;Ext.createInterceptor=Ext.util.Functions.createInterceptor;Ext.createSequence=Ext.util.Functions.createSequence;Ext.createDelegate=Ext.util.Functions.createDelegate;Ext.apply(Ext.util.Observable.prototype,function(){function getMethodEvent(method){var e=(this.methodEvents=this.methodEvents||{})[method],returnValue,v,cancel,obj=this;if(!e){this.methodEvents[method]=e={};e.originalFn=this[method];e.methodName=method;e.before=[];e.after=[];var makeCall=function(fn,scope,args){if((v=fn.apply(scope||obj,args))!==undefined){if(typeof v=='object'){if(v.returnValue!==undefined){returnValue=v.returnValue;}else{returnValue=v;} +cancel=!!v.cancel;} +else +if(v===false){cancel=true;} +else{returnValue=v;}}};this[method]=function(){var args=Array.prototype.slice.call(arguments,0),b;returnValue=v=undefined;cancel=false;for(var i=0,len=e.before.length;i<len;i++){b=e.before[i];makeCall(b.fn,b.scope,args);if(cancel){return returnValue;}} +if((v=e.originalFn.apply(obj,args))!==undefined){returnValue=v;} +for(var i=0,len=e.after.length;i<len;i++){b=e.after[i];makeCall(b.fn,b.scope,args);if(cancel){return returnValue;}} +return returnValue;};} +return e;} +return{beforeMethod:function(method,fn,scope){getMethodEvent.call(this,method).before.push({fn:fn,scope:scope});},afterMethod:function(method,fn,scope){getMethodEvent.call(this,method).after.push({fn:fn,scope:scope});},removeMethodListener:function(method,fn,scope){var e=this.getMethodEvent(method);for(var i=0,len=e.before.length;i<len;i++){if(e.before[i].fn==fn&&e.before[i].scope==scope){e.before.splice(i,1);return;}} +for(var i=0,len=e.after.length;i<len;i++){if(e.after[i].fn==fn&&e.after[i].scope==scope){e.after.splice(i,1);return;}}},relayEvents:function(o,events){var me=this;function createHandler(ename){return function(){return me.fireEvent.apply(me,[ename].concat(Array.prototype.slice.call(arguments,0)));};} +for(var i=0,len=events.length;i<len;i++){var ename=events[i];me.events[ename]=me.events[ename]||true;o.on(ename,createHandler(ename),me);}},enableBubble:function(events){var me=this;if(!Ext.isEmpty(events)){events=Ext.isArray(events)?events:Array.prototype.slice.call(arguments,0);for(var i=0,len=events.length;i<len;i++){var ename=events[i];ename=ename.toLowerCase();var ce=me.events[ename]||true;if(typeof ce=='boolean'){ce=new Ext.util.Event(me,ename);me.events[ename]=ce;} +ce.bubble=true;}}}};}());Ext.util.Observable.capture=function(o,fn,scope){o.fireEvent=o.fireEvent.createInterceptor(fn,scope);};Ext.util.Observable.observeClass=function(c,listeners){if(c){if(!c.fireEvent){Ext.apply(c,new Ext.util.Observable());Ext.util.Observable.capture(c.prototype,c.fireEvent,c);} +if(typeof listeners=='object'){c.on(listeners);} +return c;}};Ext.apply(Ext.EventManager,function(){var resizeEvent,resizeTask,textEvent,textSize,D=Ext.lib.Dom,propRe=/^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/,curWidth=0,curHeight=0,useKeydown=Ext.isWebKit?Ext.num(navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1])>=525:!((Ext.isGecko&&!Ext.isWindows)||Ext.isOpera);return{doResizeEvent:function(){var h=D.getViewHeight(),w=D.getViewWidth();if(curHeight!=h||curWidth!=w){resizeEvent.fire(curWidth=w,curHeight=h);}},onWindowResize:function(fn,scope,options){if(!resizeEvent){resizeEvent=new Ext.util.Event();resizeTask=new Ext.util.DelayedTask(this.doResizeEvent);Ext.EventManager.on(window,"resize",this.fireWindowResize,this);} +resizeEvent.addListener(fn,scope,options);},fireWindowResize:function(){if(resizeEvent){resizeTask.delay(100);}},onTextResize:function(fn,scope,options){if(!textEvent){textEvent=new Ext.util.Event();var textEl=new Ext.Element(document.createElement('div'));textEl.dom.className='x-text-resize';textEl.dom.innerHTML='X';textEl.appendTo(document.body);textSize=textEl.dom.offsetHeight;setInterval(function(){if(textEl.dom.offsetHeight!=textSize){textEvent.fire(textSize,textSize=textEl.dom.offsetHeight);}},this.textResizeInterval);} +textEvent.addListener(fn,scope,options);},removeResizeListener:function(fn,scope){if(resizeEvent){resizeEvent.removeListener(fn,scope);}},fireResize:function(){if(resizeEvent){resizeEvent.fire(D.getViewWidth(),D.getViewHeight());}},textResizeInterval:50,ieDeferSrc:false,getKeyEvent:function(){return useKeydown?'keydown':'keypress';},useKeydown:useKeydown};}());Ext.EventManager.on=Ext.EventManager.addListener;Ext.apply(Ext.EventObjectImpl.prototype,{BACKSPACE:8,TAB:9,NUM_CENTER:12,ENTER:13,RETURN:13,SHIFT:16,CTRL:17,CONTROL:17,ALT:18,PAUSE:19,CAPS_LOCK:20,ESC:27,SPACE:32,PAGE_UP:33,PAGEUP:33,PAGE_DOWN:34,PAGEDOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,PRINT_SCREEN:44,INSERT:45,DELETE:46,ZERO:48,ONE:49,TWO:50,THREE:51,FOUR:52,FIVE:53,SIX:54,SEVEN:55,EIGHT:56,NINE:57,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90,CONTEXT_MENU:93,NUM_ZERO:96,NUM_ONE:97,NUM_TWO:98,NUM_THREE:99,NUM_FOUR:100,NUM_FIVE:101,NUM_SIX:102,NUM_SEVEN:103,NUM_EIGHT:104,NUM_NINE:105,NUM_MULTIPLY:106,NUM_PLUS:107,NUM_MINUS:109,NUM_PERIOD:110,NUM_DIVISION:111,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,isNavKeyPress:function(){var me=this,k=this.normalizeKey(me.keyCode);return(k>=33&&k<=40)||k==me.RETURN||k==me.TAB||k==me.ESC;},isSpecialKey:function(){var k=this.normalizeKey(this.keyCode);return(this.type=='keypress'&&this.ctrlKey)||this.isNavKeyPress()||(k==this.BACKSPACE)||(k>=16&&k<=20)||(k>=44&&k<=46);},getPoint:function(){return new Ext.lib.Point(this.xy[0],this.xy[1]);},hasModifier:function(){return((this.ctrlKey||this.altKey)||this.shiftKey);}});Ext.Element.addMethods({swallowEvent:function(eventName,preventDefault){var me=this;function fn(e){e.stopPropagation();if(preventDefault){e.preventDefault();}} +if(Ext.isArray(eventName)){Ext.each(eventName,function(e){me.on(e,fn);});return me;} +me.on(eventName,fn);return me;},relayEvent:function(eventName,observable){this.on(eventName,function(e){observable.fireEvent(eventName,e);});},clean:function(forceReclean){var me=this,dom=me.dom,n=dom.firstChild,ni=-1;if(Ext.Element.data(dom,'isCleaned')&&forceReclean!==true){return me;} +while(n){var nx=n.nextSibling;if(n.nodeType==3&&!(/\S/.test(n.nodeValue))){dom.removeChild(n);}else{n.nodeIndex=++ni;} +n=nx;} +Ext.Element.data(dom,'isCleaned',true);return me;},load:function(){var updateManager=this.getUpdater();updateManager.update.apply(updateManager,arguments);return this;},getUpdater:function(){return this.updateManager||(this.updateManager=new Ext.Updater(this));},update:function(html,loadScripts,callback){if(!this.dom){return this;} +html=html||"";if(loadScripts!==true){this.dom.innerHTML=html;if(typeof callback=='function'){callback();} +return this;} +var id=Ext.id(),dom=this.dom;html+='<span id="'+id+'"></span>';Ext.lib.Event.onAvailable(id,function(){var DOC=document,hd=DOC.getElementsByTagName("head")[0],re=/(?:<script([^>]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig,srcRe=/\ssrc=([\'\"])(.*?)\1/i,typeRe=/\stype=([\'\"])(.*?)\1/i,match,attrs,srcMatch,typeMatch,el,s;while((match=re.exec(html))){attrs=match[1];srcMatch=attrs?attrs.match(srcRe):false;if(srcMatch&&srcMatch[2]){s=DOC.createElement("script");s.src=srcMatch[2];typeMatch=attrs.match(typeRe);if(typeMatch&&typeMatch[2]){s.type=typeMatch[2];} +hd.appendChild(s);}else if(match[2]&&match[2].length>0){if(window.execScript){window.execScript(match[2]);}else{window.eval(match[2]);}}} +el=DOC.getElementById(id);if(el){Ext.removeNode(el);} +if(typeof callback=='function'){callback();}});dom.innerHTML=html.replace(/(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig,"");return this;},removeAllListeners:function(){this.removeAnchor();Ext.EventManager.removeAll(this.dom);return this;},createProxy:function(config,renderTo,matchBox){config=(typeof config=='object')?config:{tag:"div",cls:config};var me=this,proxy=renderTo?Ext.DomHelper.append(renderTo,config,true):Ext.DomHelper.insertBefore(me.dom,config,true);if(matchBox&&me.setBox&&me.getBox){proxy.setBox(me.getBox());} +return proxy;}});Ext.Element.prototype.getUpdateManager=Ext.Element.prototype.getUpdater;Ext.Element.addMethods({getAnchorXY:function(anchor,local,s){anchor=(anchor||"tl").toLowerCase();s=s||{};var me=this,vp=me.dom==document.body||me.dom==document,w=s.width||vp?Ext.lib.Dom.getViewWidth():me.getWidth(),h=s.height||vp?Ext.lib.Dom.getViewHeight():me.getHeight(),xy,r=Math.round,o=me.getXY(),scroll=me.getScroll(),extraX=vp?scroll.left:!local?o[0]:0,extraY=vp?scroll.top:!local?o[1]:0,hash={c:[r(w*0.5),r(h*0.5)],t:[r(w*0.5),0],l:[0,r(h*0.5)],r:[w,r(h*0.5)],b:[r(w*0.5),h],tl:[0,0],bl:[0,h],br:[w,h],tr:[w,0]};xy=hash[anchor];return[xy[0]+extraX,xy[1]+extraY];},anchorTo:function(el,alignment,offsets,animate,monitorScroll,callback){var me=this,dom=me.dom,scroll=!Ext.isEmpty(monitorScroll),action=function(){Ext.fly(dom).alignTo(el,alignment,offsets,animate);Ext.callback(callback,Ext.fly(dom));},anchor=this.getAnchor();this.removeAnchor();Ext.apply(anchor,{fn:action,scroll:scroll});Ext.EventManager.onWindowResize(action,null);if(scroll){Ext.EventManager.on(window,'scroll',action,null,{buffer:!isNaN(monitorScroll)?monitorScroll:50});} +action.call(me);return me;},removeAnchor:function(){var me=this,anchor=this.getAnchor();if(anchor&&anchor.fn){Ext.EventManager.removeResizeListener(anchor.fn);if(anchor.scroll){Ext.EventManager.un(window,'scroll',anchor.fn);} +delete anchor.fn;} +return me;},getAnchor:function(){var data=Ext.Element.data,dom=this.dom;if(!dom){return;} +var anchor=data(dom,'_anchor');if(!anchor){anchor=data(dom,'_anchor',{});} +return anchor;},getAlignToXY:function(el,p,o){el=Ext.get(el);if(!el||!el.dom){throw"Element.alignToXY with an element that doesn't exist";} +o=o||[0,0];p=(!p||p=="?"?"tl-bl?":(!(/-/).test(p)&&p!==""?"tl-"+p:p||"tl-bl")).toLowerCase();var me=this,d=me.dom,a1,a2,x,y,w,h,r,dw=Ext.lib.Dom.getViewWidth()-10,dh=Ext.lib.Dom.getViewHeight()-10,p1y,p1x,p2y,p2x,swapY,swapX,doc=document,docElement=doc.documentElement,docBody=doc.body,scrollX=(docElement.scrollLeft||docBody.scrollLeft||0)+5,scrollY=(docElement.scrollTop||docBody.scrollTop||0)+5,c=false,p1="",p2="",m=p.match(/^([a-z]+)-([a-z]+)(\?)?$/);if(!m){throw"Element.alignTo with an invalid alignment "+p;} +p1=m[1];p2=m[2];c=!!m[3];a1=me.getAnchorXY(p1,true);a2=el.getAnchorXY(p2,false);x=a2[0]-a1[0]+o[0];y=a2[1]-a1[1]+o[1];if(c){w=me.getWidth();h=me.getHeight();r=el.getRegion();p1y=p1.charAt(0);p1x=p1.charAt(p1.length-1);p2y=p2.charAt(0);p2x=p2.charAt(p2.length-1);swapY=((p1y=="t"&&p2y=="b")||(p1y=="b"&&p2y=="t"));swapX=((p1x=="r"&&p2x=="l")||(p1x=="l"&&p2x=="r"));if(x+w>dw+scrollX){x=swapX?r.left-w:dw+scrollX-w;} +if(x<scrollX){x=swapX?r.right:scrollX;} +if(y+h>dh+scrollY){y=swapY?r.top-h:dh+scrollY-h;} +if(y<scrollY){y=swapY?r.bottom:scrollY;}} +return[x,y];},alignTo:function(element,position,offsets,animate){var me=this;return me.setXY(me.getAlignToXY(element,position,offsets),me.preanim&&!!animate?me.preanim(arguments,3):false);},adjustForConstraints:function(xy,parent,offsets){return this.getConstrainToXY(parent||document,false,offsets,xy)||xy;},getConstrainToXY:function(el,local,offsets,proposedXY){var os={top:0,left:0,bottom:0,right:0};return function(el,local,offsets,proposedXY){el=Ext.get(el);offsets=offsets?Ext.applyIf(offsets,os):os;var vw,vh,vx=0,vy=0;if(el.dom==document.body||el.dom==document){vw=Ext.lib.Dom.getViewWidth();vh=Ext.lib.Dom.getViewHeight();}else{vw=el.dom.clientWidth;vh=el.dom.clientHeight;if(!local){var vxy=el.getXY();vx=vxy[0];vy=vxy[1];}} +var s=el.getScroll();vx+=offsets.left+s.left;vy+=offsets.top+s.top;vw-=offsets.right;vh-=offsets.bottom;var vr=vx+vw,vb=vy+vh,xy=proposedXY||(!local?this.getXY():[this.getLeft(true),this.getTop(true)]),x=xy[0],y=xy[1],offset=this.getConstrainOffset(),w=this.dom.offsetWidth+offset,h=this.dom.offsetHeight+offset;var moved=false;if((x+w)>vr){x=vr-w;moved=true;} +if((y+h)>vb){y=vb-h;moved=true;} +if(x<vx){x=vx;moved=true;} +if(y<vy){y=vy;moved=true;} +return moved?[x,y]:false;};}(),getConstrainOffset:function(){return 0;},getCenterXY:function(){return this.getAlignToXY(document,'c-c');},center:function(centerIn){return this.alignTo(centerIn||document,'c-c');}});Ext.Element.addMethods({select:function(selector,unique){return Ext.Element.select(selector,unique,this.dom);}});Ext.apply(Ext.Element.prototype,function(){var GETDOM=Ext.getDom,GET=Ext.get,DH=Ext.DomHelper;return{insertSibling:function(el,where,returnDom){var me=this,rt,isAfter=(where||'before').toLowerCase()=='after',insertEl;if(Ext.isArray(el)){insertEl=me;Ext.each(el,function(e){rt=Ext.fly(insertEl,'_internal').insertSibling(e,where,returnDom);if(isAfter){insertEl=rt;}});return rt;} +el=el||{};if(el.nodeType||el.dom){rt=me.dom.parentNode.insertBefore(GETDOM(el),isAfter?me.dom.nextSibling:me.dom);if(!returnDom){rt=GET(rt);}}else{if(isAfter&&!me.dom.nextSibling){rt=DH.append(me.dom.parentNode,el,!returnDom);}else{rt=DH[isAfter?'insertAfter':'insertBefore'](me.dom,el,!returnDom);}} +return rt;}};}());Ext.Element.boxMarkup='<div class="{0}-tl"><div class="{0}-tr"><div class="{0}-tc"></div></div></div><div class="{0}-ml"><div class="{0}-mr"><div class="{0}-mc"></div></div></div><div class="{0}-bl"><div class="{0}-br"><div class="{0}-bc"></div></div></div>';Ext.Element.addMethods(function(){var INTERNAL="_internal",pxMatch=/(\d+\.?\d+)px/;return{applyStyles:function(style){Ext.DomHelper.applyStyles(this.dom,style);return this;},getStyles:function(){var ret={};Ext.each(arguments,function(v){ret[v]=this.getStyle(v);},this);return ret;},setOverflow:function(v){var dom=this.dom;if(v=='auto'&&Ext.isMac&&Ext.isGecko2){dom.style.overflow='hidden';(function(){dom.style.overflow='auto';}).defer(1);}else{dom.style.overflow=v;}},boxWrap:function(cls){cls=cls||'x-box';var el=Ext.get(this.insertHtml("beforeBegin","<div class='"+cls+"'>"+String.format(Ext.Element.boxMarkup,cls)+"</div>"));Ext.DomQuery.selectNode('.'+cls+'-mc',el.dom).appendChild(this.dom);return el;},setSize:function(width,height,animate){var me=this;if(typeof width=='object'){height=width.height;width=width.width;} +width=me.adjustWidth(width);height=me.adjustHeight(height);if(!animate||!me.anim){me.dom.style.width=me.addUnits(width);me.dom.style.height=me.addUnits(height);}else{me.anim({width:{to:width},height:{to:height}},me.preanim(arguments,2));} +return me;},getComputedHeight:function(){var me=this,h=Math.max(me.dom.offsetHeight,me.dom.clientHeight);if(!h){h=parseFloat(me.getStyle('height'))||0;if(!me.isBorderBox()){h+=me.getFrameWidth('tb');}} +return h;},getComputedWidth:function(){var w=Math.max(this.dom.offsetWidth,this.dom.clientWidth);if(!w){w=parseFloat(this.getStyle('width'))||0;if(!this.isBorderBox()){w+=this.getFrameWidth('lr');}} +return w;},getFrameWidth:function(sides,onlyContentBox){return onlyContentBox&&this.isBorderBox()?0:(this.getPadding(sides)+this.getBorderWidth(sides));},addClassOnOver:function(className){this.hover(function(){Ext.fly(this,INTERNAL).addClass(className);},function(){Ext.fly(this,INTERNAL).removeClass(className);});return this;},addClassOnFocus:function(className){this.on("focus",function(){Ext.fly(this,INTERNAL).addClass(className);},this.dom);this.on("blur",function(){Ext.fly(this,INTERNAL).removeClass(className);},this.dom);return this;},addClassOnClick:function(className){var dom=this.dom;this.on("mousedown",function(){Ext.fly(dom,INTERNAL).addClass(className);var d=Ext.getDoc(),fn=function(){Ext.fly(dom,INTERNAL).removeClass(className);d.removeListener("mouseup",fn);};d.on("mouseup",fn);});return this;},getViewSize:function(){var doc=document,d=this.dom,isDoc=(d==doc||d==doc.body);if(isDoc){var extdom=Ext.lib.Dom;return{width:extdom.getViewWidth(),height:extdom.getViewHeight()};}else{return{width:d.clientWidth,height:d.clientHeight};}},getStyleSize:function(){var me=this,w,h,doc=document,d=this.dom,isDoc=(d==doc||d==doc.body),s=d.style;if(isDoc){var extdom=Ext.lib.Dom;return{width:extdom.getViewWidth(),height:extdom.getViewHeight()};} +if(s.width&&s.width!='auto'){w=parseFloat(s.width);if(me.isBorderBox()){w-=me.getFrameWidth('lr');}} +if(s.height&&s.height!='auto'){h=parseFloat(s.height);if(me.isBorderBox()){h-=me.getFrameWidth('tb');}} +return{width:w||me.getWidth(true),height:h||me.getHeight(true)};},getSize:function(contentSize){return{width:this.getWidth(contentSize),height:this.getHeight(contentSize)};},repaint:function(){var dom=this.dom;this.addClass("x-repaint");setTimeout(function(){Ext.fly(dom).removeClass("x-repaint");},1);return this;},unselectable:function(){this.dom.unselectable="on";return this.swallowEvent("selectstart",true).applyStyles("-moz-user-select:none;-khtml-user-select:none;").addClass("x-unselectable");},getMargins:function(side){var me=this,key,hash={t:"top",l:"left",r:"right",b:"bottom"},o={};if(!side){for(key in me.margins){o[hash[key]]=parseFloat(me.getStyle(me.margins[key]))||0;} +return o;}else{return me.addStyles.call(me,side,me.margins);}}};}());Ext.Element.addMethods({setBox:function(box,adjust,animate){var me=this,w=box.width,h=box.height;if((adjust&&!me.autoBoxAdjust)&&!me.isBorderBox()){w-=(me.getBorderWidth("lr")+me.getPadding("lr"));h-=(me.getBorderWidth("tb")+me.getPadding("tb"));} +me.setBounds(box.x,box.y,w,h,me.animTest.call(me,arguments,animate,2));return me;},getBox:function(contentBox,local){var me=this,xy,left,top,getBorderWidth=me.getBorderWidth,getPadding=me.getPadding,l,r,t,b;if(!local){xy=me.getXY();}else{left=parseInt(me.getStyle("left"),10)||0;top=parseInt(me.getStyle("top"),10)||0;xy=[left,top];} +var el=me.dom,w=el.offsetWidth,h=el.offsetHeight,bx;if(!contentBox){bx={x:xy[0],y:xy[1],0:xy[0],1:xy[1],width:w,height:h};}else{l=getBorderWidth.call(me,"l")+getPadding.call(me,"l");r=getBorderWidth.call(me,"r")+getPadding.call(me,"r");t=getBorderWidth.call(me,"t")+getPadding.call(me,"t");b=getBorderWidth.call(me,"b")+getPadding.call(me,"b");bx={x:xy[0]+l,y:xy[1]+t,0:xy[0]+l,1:xy[1]+t,width:w-(l+r),height:h-(t+b)};} +bx.right=bx.x+bx.width;bx.bottom=bx.y+bx.height;return bx;},move:function(direction,distance,animate){var me=this,xy=me.getXY(),x=xy[0],y=xy[1],left=[x-distance,y],right=[x+distance,y],top=[x,y-distance],bottom=[x,y+distance],hash={l:left,left:left,r:right,right:right,t:top,top:top,up:top,b:bottom,bottom:bottom,down:bottom};direction=direction.toLowerCase();me.moveTo(hash[direction][0],hash[direction][1],me.animTest.call(me,arguments,animate,2));},setLeftTop:function(left,top){var me=this,style=me.dom.style;style.left=me.addUnits(left);style.top=me.addUnits(top);return me;},getRegion:function(){return Ext.lib.Dom.getRegion(this.dom);},setBounds:function(x,y,width,height,animate){var me=this;if(!animate||!me.anim){me.setSize(width,height);me.setLocation(x,y);}else{me.anim({points:{to:[x,y]},width:{to:me.adjustWidth(width)},height:{to:me.adjustHeight(height)}},me.preanim(arguments,4),'motion');} +return me;},setRegion:function(region,animate){return this.setBounds(region.left,region.top,region.right-region.left,region.bottom-region.top,this.animTest.call(this,arguments,animate,1));}});Ext.Element.addMethods({scrollTo:function(side,value,animate){var top=/top/i.test(side),me=this,dom=me.dom,prop;if(!animate||!me.anim){prop='scroll'+(top?'Top':'Left');dom[prop]=value;} +else{prop='scroll'+(top?'Left':'Top');me.anim({scroll:{to:top?[dom[prop],value]:[value,dom[prop]]}},me.preanim(arguments,2),'scroll');} +return me;},scrollIntoView:function(container,hscroll){var c=Ext.getDom(container)||Ext.getBody().dom,el=this.dom,o=this.getOffsetsTo(c),l=o[0]+c.scrollLeft,t=o[1]+c.scrollTop,b=t+el.offsetHeight,r=l+el.offsetWidth,ch=c.clientHeight,ct=parseInt(c.scrollTop,10),cl=parseInt(c.scrollLeft,10),cb=ct+ch,cr=cl+c.clientWidth;if(el.offsetHeight>ch||t<ct){c.scrollTop=t;} +else if(b>cb){c.scrollTop=b-ch;} +c.scrollTop=c.scrollTop;if(hscroll!==false){if(el.offsetWidth>c.clientWidth||l<cl){c.scrollLeft=l;} +else if(r>cr){c.scrollLeft=r-c.clientWidth;} +c.scrollLeft=c.scrollLeft;} +return this;},scrollChildIntoView:function(child,hscroll){Ext.fly(child,'_scrollChildIntoView').scrollIntoView(this,hscroll);},scroll:function(direction,distance,animate){if(!this.isScrollable()){return false;} +var el=this.dom,l=el.scrollLeft,t=el.scrollTop,w=el.scrollWidth,h=el.scrollHeight,cw=el.clientWidth,ch=el.clientHeight,scrolled=false,v,hash={l:Math.min(l+distance,w-cw),r:v=Math.max(l-distance,0),t:Math.max(t-distance,0),b:Math.min(t+distance,h-ch)};hash.d=hash.b;hash.u=hash.t;direction=direction.substr(0,1);if((v=hash[direction])>-1){scrolled=true;this.scrollTo(direction=='l'||direction=='r'?'left':'top',v,this.preanim(arguments,2));} +return scrolled;}});Ext.Element.addMethods(function(){var VISIBILITY="visibility",DISPLAY="display",HIDDEN="hidden",NONE="none",XMASKED="x-masked",XMASKEDRELATIVE="x-masked-relative",data=Ext.Element.data;return{isVisible:function(deep){var vis=!this.isStyle(VISIBILITY,HIDDEN)&&!this.isStyle(DISPLAY,NONE),p=this.dom.parentNode;if(deep!==true||!vis){return vis;} +while(p&&!(/^body/i.test(p.tagName))){if(!Ext.fly(p,'_isVisible').isVisible()){return false;} +p=p.parentNode;} +return true;},isDisplayed:function(){return!this.isStyle(DISPLAY,NONE);},enableDisplayMode:function(display){this.setVisibilityMode(Ext.Element.DISPLAY);if(!Ext.isEmpty(display)){data(this.dom,'originalDisplay',display);} +return this;},mask:function(msg,msgCls){var me=this,dom=me.dom,dh=Ext.DomHelper,EXTELMASKMSG="ext-el-mask-msg",el,mask;if(!(/^body/i.test(dom.tagName)&&me.getStyle('position')=='static')){me.addClass(XMASKEDRELATIVE);} +if(el=data(dom,'maskMsg')){el.remove();} +if(el=data(dom,'mask')){el.remove();} +mask=dh.append(dom,{cls:"ext-el-mask"},true);data(dom,'mask',mask);me.addClass(XMASKED);mask.setDisplayed(true);if(typeof msg=='string'){var mm=dh.append(dom,{cls:EXTELMASKMSG,cn:{tag:'div'}},true);data(dom,'maskMsg',mm);mm.dom.className=msgCls?EXTELMASKMSG+" "+msgCls:EXTELMASKMSG;mm.dom.firstChild.innerHTML=msg;mm.setDisplayed(true);mm.center(me);} +if(Ext.isIE&&!(Ext.isIE7&&Ext.isStrict)&&me.getStyle('height')=='auto'){mask.setSize(undefined,me.getHeight());} +return mask;},unmask:function(){var me=this,dom=me.dom,mask=data(dom,'mask'),maskMsg=data(dom,'maskMsg');if(mask){if(maskMsg){maskMsg.remove();data(dom,'maskMsg',undefined);} +mask.remove();data(dom,'mask',undefined);me.removeClass([XMASKED,XMASKEDRELATIVE]);}},isMasked:function(){var m=data(this.dom,'mask');return m&&m.isVisible();},createShim:function(){var el=document.createElement('iframe'),shim;el.frameBorder='0';el.className='ext-shim';el.src=Ext.SSL_SECURE_URL;shim=Ext.get(this.dom.parentNode.insertBefore(el,this.dom));shim.autoBoxAdjust=false;return shim;}};}());Ext.Element.addMethods({addKeyListener:function(key,fn,scope){var config;if(typeof key!='object'||Ext.isArray(key)){config={key:key,fn:fn,scope:scope};}else{config={key:key.key,shift:key.shift,ctrl:key.ctrl,alt:key.alt,fn:fn,scope:scope};} +return new Ext.KeyMap(this,config);},addKeyMap:function(config){return new Ext.KeyMap(this,config);}});Ext.CompositeElementLite.importElementMethods();Ext.apply(Ext.CompositeElementLite.prototype,{addElements:function(els,root){if(!els){return this;} +if(typeof els=="string"){els=Ext.Element.selectorFunction(els,root);} +var yels=this.elements;Ext.each(els,function(e){yels.push(Ext.get(e));});return this;},first:function(){return this.item(0);},last:function(){return this.item(this.getCount()-1);},contains:function(el){return this.indexOf(el)!=-1;},removeElement:function(keys,removeDom){var me=this,els=this.elements,el;Ext.each(keys,function(val){if((el=(els[val]||els[val=me.indexOf(val)]))){if(removeDom){if(el.dom){el.remove();}else{Ext.removeNode(el);}} +els.splice(val,1);}});return this;}});Ext.CompositeElement=Ext.extend(Ext.CompositeElementLite,{constructor:function(els,root){this.elements=[];this.add(els,root);},getElement:function(el){return el;},transformElement:function(el){return Ext.get(el);}});Ext.Element.select=function(selector,unique,root){var els;if(typeof selector=="string"){els=Ext.Element.selectorFunction(selector,root);}else if(selector.length!==undefined){els=selector;}else{throw"Invalid selector";} +return(unique===true)?new Ext.CompositeElement(els):new Ext.CompositeElementLite(els);};Ext.select=Ext.Element.select;Ext.UpdateManager=Ext.Updater=Ext.extend(Ext.util.Observable,function(){var BEFOREUPDATE="beforeupdate",UPDATE="update",FAILURE="failure";function processSuccess(response){var me=this;me.transaction=null;if(response.argument.form&&response.argument.reset){try{response.argument.form.reset();}catch(e){}} +if(me.loadScripts){me.renderer.render(me.el,response,me,updateComplete.createDelegate(me,[response]));}else{me.renderer.render(me.el,response,me);updateComplete.call(me,response);}} +function updateComplete(response,type,success){this.fireEvent(type||UPDATE,this.el,response);if(Ext.isFunction(response.argument.callback)){response.argument.callback.call(response.argument.scope,this.el,Ext.isEmpty(success)?true:false,response,response.argument.options);}} +function processFailure(response){updateComplete.call(this,response,FAILURE,!!(this.transaction=null));} +return{constructor:function(el,forceNew){var me=this;el=Ext.get(el);if(!forceNew&&el.updateManager){return el.updateManager;} +me.el=el;me.defaultUrl=null;me.addEvents(BEFOREUPDATE,UPDATE,FAILURE);Ext.apply(me,Ext.Updater.defaults);me.transaction=null;me.refreshDelegate=me.refresh.createDelegate(me);me.updateDelegate=me.update.createDelegate(me);me.formUpdateDelegate=(me.formUpdate||function(){}).createDelegate(me);me.renderer=me.renderer||me.getDefaultRenderer();Ext.Updater.superclass.constructor.call(me);},setRenderer:function(renderer){this.renderer=renderer;},getRenderer:function(){return this.renderer;},getDefaultRenderer:function(){return new Ext.Updater.BasicRenderer();},setDefaultUrl:function(defaultUrl){this.defaultUrl=defaultUrl;},getEl:function(){return this.el;},update:function(url,params,callback,discardUrl){var me=this,cfg,callerScope;if(me.fireEvent(BEFOREUPDATE,me.el,url,params)!==false){if(Ext.isObject(url)){cfg=url;url=cfg.url;params=params||cfg.params;callback=callback||cfg.callback;discardUrl=discardUrl||cfg.discardUrl;callerScope=cfg.scope;if(!Ext.isEmpty(cfg.nocache)){me.disableCaching=cfg.nocache;};if(!Ext.isEmpty(cfg.text)){me.indicatorText='<div class="loading-indicator">'+cfg.text+"</div>";};if(!Ext.isEmpty(cfg.scripts)){me.loadScripts=cfg.scripts;};if(!Ext.isEmpty(cfg.timeout)){me.timeout=cfg.timeout;};} +me.showLoading();if(!discardUrl){me.defaultUrl=url;} +if(Ext.isFunction(url)){url=url.call(me);} +var o=Ext.apply({},{url:url,params:(Ext.isFunction(params)&&callerScope)?params.createDelegate(callerScope):params,success:processSuccess,failure:processFailure,scope:me,callback:undefined,timeout:(me.timeout*1000),disableCaching:me.disableCaching,argument:{"options":cfg,"url":url,"form":null,"callback":callback,"scope":callerScope||window,"params":params}},cfg);me.transaction=Ext.Ajax.request(o);}},formUpdate:function(form,url,reset,callback){var me=this;if(me.fireEvent(BEFOREUPDATE,me.el,form,url)!==false){if(Ext.isFunction(url)){url=url.call(me);} +form=Ext.getDom(form);me.transaction=Ext.Ajax.request({form:form,url:url,success:processSuccess,failure:processFailure,scope:me,timeout:(me.timeout*1000),argument:{"url":url,"form":form,"callback":callback,"reset":reset}});me.showLoading.defer(1,me);}},startAutoRefresh:function(interval,url,params,callback,refreshNow){var me=this;if(refreshNow){me.update(url||me.defaultUrl,params,callback,true);} +if(me.autoRefreshProcId){clearInterval(me.autoRefreshProcId);} +me.autoRefreshProcId=setInterval(me.update.createDelegate(me,[url||me.defaultUrl,params,callback,true]),interval*1000);},stopAutoRefresh:function(){if(this.autoRefreshProcId){clearInterval(this.autoRefreshProcId);delete this.autoRefreshProcId;}},isAutoRefreshing:function(){return!!this.autoRefreshProcId;},showLoading:function(){if(this.showLoadIndicator){this.el.dom.innerHTML=this.indicatorText;}},abort:function(){if(this.transaction){Ext.Ajax.abort(this.transaction);}},isUpdating:function(){return this.transaction?Ext.Ajax.isLoading(this.transaction):false;},refresh:function(callback){if(this.defaultUrl){this.update(this.defaultUrl,null,callback,true);}}};}());Ext.Updater.defaults={timeout:30,disableCaching:false,showLoadIndicator:true,indicatorText:'<div class="loading-indicator">Loading...</div>',loadScripts:false,sslBlankUrl:Ext.SSL_SECURE_URL};Ext.Updater.updateElement=function(el,url,params,options){var um=Ext.get(el).getUpdater();Ext.apply(um,options);um.update(url,params,options?options.callback:null);};Ext.Updater.BasicRenderer=function(){};Ext.Updater.BasicRenderer.prototype={render:function(el,response,updateManager,callback){el.update(response.responseText,updateManager.loadScripts,callback);}};(function(){Date.useStrict=false;function xf(format){var args=Array.prototype.slice.call(arguments,1);return format.replace(/\{(\d+)\}/g,function(m,i){return args[i];});} +Date.formatCodeToRegex=function(character,currentGroup){var p=Date.parseCodes[character];if(p){p=typeof p=='function'?p():p;Date.parseCodes[character]=p;} +return p?Ext.applyIf({c:p.c?xf(p.c,currentGroup||"{0}"):p.c},p):{g:0,c:null,s:Ext.escapeRe(character)};};var $f=Date.formatCodeToRegex;Ext.apply(Date,{parseFunctions:{"M$":function(input,strict){var re=new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/');var r=(input||'').match(re);return r?new Date(((r[1]||'')+r[2])*1):null;}},parseRegexes:[],formatFunctions:{"M$":function(){return'\\/Date('+this.getTime()+')\\/';}},y2kYear:50,MILLI:"ms",SECOND:"s",MINUTE:"mi",HOUR:"h",DAY:"d",MONTH:"mo",YEAR:"y",defaults:{},dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNumbers:{Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11},getShortMonthName:function(month){return Date.monthNames[month].substring(0,3);},getShortDayName:function(day){return Date.dayNames[day].substring(0,3);},getMonthNumber:function(name){return Date.monthNumbers[name.substring(0,1).toUpperCase()+name.substring(1,3).toLowerCase()];},formatCodes:{d:"String.leftPad(this.getDate(), 2, '0')",D:"Date.getShortDayName(this.getDay())",j:"this.getDate()",l:"Date.dayNames[this.getDay()]",N:"(this.getDay() ? this.getDay() : 7)",S:"this.getSuffix()",w:"this.getDay()",z:"this.getDayOfYear()",W:"String.leftPad(this.getWeekOfYear(), 2, '0')",F:"Date.monthNames[this.getMonth()]",m:"String.leftPad(this.getMonth() + 1, 2, '0')",M:"Date.getShortMonthName(this.getMonth())",n:"(this.getMonth() + 1)",t:"this.getDaysInMonth()",L:"(this.isLeapYear() ? 1 : 0)",o:"(this.getFullYear() + (this.getWeekOfYear() == 1 && this.getMonth() > 0 ? +1 : (this.getWeekOfYear() >= 52 && this.getMonth() < 11 ? -1 : 0)))",Y:"String.leftPad(this.getFullYear(), 4, '0')",y:"('' + this.getFullYear()).substring(2, 4)",a:"(this.getHours() < 12 ? 'am' : 'pm')",A:"(this.getHours() < 12 ? 'AM' : 'PM')",g:"((this.getHours() % 12) ? this.getHours() % 12 : 12)",G:"this.getHours()",h:"String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",H:"String.leftPad(this.getHours(), 2, '0')",i:"String.leftPad(this.getMinutes(), 2, '0')",s:"String.leftPad(this.getSeconds(), 2, '0')",u:"String.leftPad(this.getMilliseconds(), 3, '0')",O:"this.getGMTOffset()",P:"this.getGMTOffset(true)",T:"this.getTimezone()",Z:"(this.getTimezoneOffset() * -60)",c:function(){for(var c="Y-m-dTH:i:sP",code=[],i=0,l=c.length;i<l;++i){var e=c.charAt(i);code.push(e=="T"?"'T'":Date.getFormatCode(e));} +return code.join(" + ");},U:"Math.round(this.getTime() / 1000)"},isValid:function(y,m,d,h,i,s,ms){h=h||0;i=i||0;s=s||0;ms=ms||0;var dt=new Date(y<100?100:y,m-1,d,h,i,s,ms).add(Date.YEAR,y<100?y-100:0);return y==dt.getFullYear()&&m==dt.getMonth()+1&&d==dt.getDate()&&h==dt.getHours()&&i==dt.getMinutes()&&s==dt.getSeconds()&&ms==dt.getMilliseconds();},parseDate:function(input,format,strict){var p=Date.parseFunctions;if(p[format]==null){Date.createParser(format);} +return p[format](input,Ext.isDefined(strict)?strict:Date.useStrict);},getFormatCode:function(character){var f=Date.formatCodes[character];if(f){f=typeof f=='function'?f():f;Date.formatCodes[character]=f;} +return f||("'"+String.escape(character)+"'");},createFormat:function(format){var code=[],special=false,ch='';for(var i=0;i<format.length;++i){ch=format.charAt(i);if(!special&&ch=="\\"){special=true;}else if(special){special=false;code.push("'"+String.escape(ch)+"'");}else{code.push(Date.getFormatCode(ch));}} +Date.formatFunctions[format]=new Function("return "+code.join('+'));},createParser:function(){var code=["var dt, y, m, d, h, i, s, ms, o, z, zz, u, v,","def = Date.defaults,","results = String(input).match(Date.parseRegexes[{0}]);","if(results){","{1}","if(u != null){","v = new Date(u * 1000);","}else{","dt = (new Date()).clearTime();","y = Ext.num(y, Ext.num(def.y, dt.getFullYear()));","m = Ext.num(m, Ext.num(def.m - 1, dt.getMonth()));","d = Ext.num(d, Ext.num(def.d, dt.getDate()));","h = Ext.num(h, Ext.num(def.h, dt.getHours()));","i = Ext.num(i, Ext.num(def.i, dt.getMinutes()));","s = Ext.num(s, Ext.num(def.s, dt.getSeconds()));","ms = Ext.num(ms, Ext.num(def.ms, dt.getMilliseconds()));","if(z >= 0 && y >= 0){","v = new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);","v = !strict? v : (strict === true && (z <= 364 || (v.isLeapYear() && z <= 365))? v.add(Date.DAY, z) : null);","}else if(strict === true && !Date.isValid(y, m + 1, d, h, i, s, ms)){","v = null;","}else{","v = new Date(y < 100 ? 100 : y, m, d, h, i, s, ms).add(Date.YEAR, y < 100 ? y - 100 : 0);","}","}","}","if(v){","if(zz != null){","v = v.add(Date.SECOND, -v.getTimezoneOffset() * 60 - zz);","}else if(o){","v = v.add(Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));","}","}","return v;"].join('\n');return function(format){var regexNum=Date.parseRegexes.length,currentGroup=1,calc=[],regex=[],special=false,ch="",i=0,obj,last;for(;i<format.length;++i){ch=format.charAt(i);if(!special&&ch=="\\"){special=true;}else if(special){special=false;regex.push(String.escape(ch));}else{obj=$f(ch,currentGroup);currentGroup+=obj.g;regex.push(obj.s);if(obj.g&&obj.c){if(obj.calcLast){last=obj.c;}else{calc.push(obj.c);}}}} +if(last){calc.push(last);} +Date.parseRegexes[regexNum]=new RegExp("^"+regex.join('')+"$",'i');Date.parseFunctions[format]=new Function("input","strict",xf(code,regexNum,calc.join('')));};}(),parseCodes:{d:{g:1,c:"d = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},j:{g:1,c:"d = parseInt(results[{0}], 10);\n",s:"(\\d{1,2})"},D:function(){for(var a=[],i=0;i<7;a.push(Date.getShortDayName(i)),++i);return{g:0,c:null,s:"(?:"+a.join("|")+")"};},l:function(){return{g:0,c:null,s:"(?:"+Date.dayNames.join("|")+")"};},N:{g:0,c:null,s:"[1-7]"},S:{g:0,c:null,s:"(?:st|nd|rd|th)"},w:{g:0,c:null,s:"[0-6]"},z:{g:1,c:"z = parseInt(results[{0}], 10);\n",s:"(\\d{1,3})"},W:{g:0,c:null,s:"(?:\\d{2})"},F:function(){return{g:1,c:"m = parseInt(Date.getMonthNumber(results[{0}]), 10);\n",s:"("+Date.monthNames.join("|")+")"};},M:function(){for(var a=[],i=0;i<12;a.push(Date.getShortMonthName(i)),++i);return Ext.applyIf({s:"("+a.join("|")+")"},$f("F"));},m:{g:1,c:"m = parseInt(results[{0}], 10) - 1;\n",s:"(\\d{2})"},n:{g:1,c:"m = parseInt(results[{0}], 10) - 1;\n",s:"(\\d{1,2})"},t:{g:0,c:null,s:"(?:\\d{2})"},L:{g:0,c:null,s:"(?:1|0)"},o:function(){return $f("Y");},Y:{g:1,c:"y = parseInt(results[{0}], 10);\n",s:"(\\d{4})"},y:{g:1,c:"var ty = parseInt(results[{0}], 10);\n" ++"y = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",s:"(\\d{1,2})"},a:function(){return $f("A");},A:{calcLast:true,g:1,c:"if (/(am)/i.test(results[{0}])) {\n" ++"if (!h || h == 12) { h = 0; }\n" ++"} else { if (!h || h < 12) { h = (h || 0) + 12; }}",s:"(AM|PM|am|pm)"},g:function(){return $f("G");},G:{g:1,c:"h = parseInt(results[{0}], 10);\n",s:"(\\d{1,2})"},h:function(){return $f("H");},H:{g:1,c:"h = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},i:{g:1,c:"i = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},s:{g:1,c:"s = parseInt(results[{0}], 10);\n",s:"(\\d{2})"},u:{g:1,c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",s:"(\\d+)"},O:{g:1,c:["o = results[{0}];","var sn = o.substring(0,1),","hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),","mn = o.substring(3,5) % 60;","o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n"].join("\n"),s:"([+\-]\\d{4})"},P:{g:1,c:["o = results[{0}];","var sn = o.substring(0,1),","hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),","mn = o.substring(4,6) % 60;","o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + String.leftPad(hr, 2, '0') + String.leftPad(mn, 2, '0')) : null;\n"].join("\n"),s:"([+\-]\\d{2}:\\d{2})"},T:{g:0,c:null,s:"[A-Z]{1,4}"},Z:{g:1,c:"zz = results[{0}] * 1;\n" ++"zz = (-43200 <= zz && zz <= 50400)? zz : null;\n",s:"([+\-]?\\d{1,5})"},c:function(){var calc=[],arr=[$f("Y",1),$f("m",2),$f("d",3),$f("h",4),$f("i",5),$f("s",6),{c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"},{c:["if(results[8]) {","if(results[8] == 'Z'){","zz = 0;","}else if (results[8].indexOf(':') > -1){",$f("P",8).c,"}else{",$f("O",8).c,"}","}"].join('\n')}];for(var i=0,l=arr.length;i<l;++i){calc.push(arr[i].c);} +return{g:1,c:calc.join(""),s:[arr[0].s,"(?:","-",arr[1].s,"(?:","-",arr[2].s,"(?:","(?:T| )?",arr[3].s,":",arr[4].s,"(?::",arr[5].s,")?","(?:(?:\\.|,)(\\d+))?","(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?",")?",")?",")?"].join("")};},U:{g:1,c:"u = parseInt(results[{0}], 10);\n",s:"(-?\\d+)"}}});}());Ext.apply(Date.prototype,{dateFormat:function(format){if(Date.formatFunctions[format]==null){Date.createFormat(format);} +return Date.formatFunctions[format].call(this);},getTimezone:function(){return this.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/,"$1$2").replace(/[^A-Z]/g,"");},getGMTOffset:function(colon){return(this.getTimezoneOffset()>0?"-":"+") ++String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset())/60),2,"0") ++(colon?":":"") ++String.leftPad(Math.abs(this.getTimezoneOffset()%60),2,"0");},getDayOfYear:function(){var num=0,d=this.clone(),m=this.getMonth(),i;for(i=0,d.setDate(1),d.setMonth(0);i<m;d.setMonth(++i)){num+=d.getDaysInMonth();} +return num+this.getDate()-1;},getWeekOfYear:function(){var ms1d=864e5,ms7d=7*ms1d;return function(){var DC3=Date.UTC(this.getFullYear(),this.getMonth(),this.getDate()+3)/ms1d,AWN=Math.floor(DC3/7),Wyr=new Date(AWN*ms7d).getUTCFullYear();return AWN-Math.floor(Date.UTC(Wyr,0,7)/ms7d)+1;};}(),isLeapYear:function(){var year=this.getFullYear();return!!((year&3)==0&&(year%100||(year%400==0&&year)));},getFirstDayOfMonth:function(){var day=(this.getDay()-(this.getDate()-1))%7;return(day<0)?(day+7):day;},getLastDayOfMonth:function(){return this.getLastDateOfMonth().getDay();},getFirstDateOfMonth:function(){return new Date(this.getFullYear(),this.getMonth(),1);},getLastDateOfMonth:function(){return new Date(this.getFullYear(),this.getMonth(),this.getDaysInMonth());},getDaysInMonth:function(){var daysInMonth=[31,28,31,30,31,30,31,31,30,31,30,31];return function(){var m=this.getMonth();return m==1&&this.isLeapYear()?29:daysInMonth[m];};}(),getSuffix:function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}},clone:function(){return new Date(this.getTime());},isDST:function(){return new Date(this.getFullYear(),0,1).getTimezoneOffset()!=this.getTimezoneOffset();},clearTime:function(clone){if(clone){return this.clone().clearTime();} +var d=this.getDate();this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);if(this.getDate()!=d){for(var hr=1,c=this.add(Date.HOUR,hr);c.getDate()!=d;hr++,c=this.add(Date.HOUR,hr));this.setDate(d);this.setHours(c.getHours());} +return this;},add:function(interval,value){var d=this.clone();if(!interval||value===0)return d;switch(interval.toLowerCase()){case Date.MILLI:d.setMilliseconds(this.getMilliseconds()+value);break;case Date.SECOND:d.setSeconds(this.getSeconds()+value);break;case Date.MINUTE:d.setMinutes(this.getMinutes()+value);break;case Date.HOUR:d.setHours(this.getHours()+value);break;case Date.DAY:d.setDate(this.getDate()+value);break;case Date.MONTH:var day=this.getDate();if(day>28){day=Math.min(day,this.getFirstDateOfMonth().add('mo',value).getLastDateOfMonth().getDate());} +d.setDate(day);d.setMonth(this.getMonth()+value);break;case Date.YEAR:d.setFullYear(this.getFullYear()+value);break;} +return d;},between:function(start,end){var t=this.getTime();return start.getTime()<=t&&t<=end.getTime();}});Date.prototype.format=Date.prototype.dateFormat;if(Ext.isSafari&&(navigator.userAgent.match(/WebKit\/(\d+)/)[1]||NaN)<420){Ext.apply(Date.prototype,{_xMonth:Date.prototype.setMonth,_xDate:Date.prototype.setDate,setMonth:function(num){if(num<=-1){var n=Math.ceil(-num),back_year=Math.ceil(n/12),month=(n%12)?12-n%12:0;this.setFullYear(this.getFullYear()-back_year);return this._xMonth(month);}else{return this._xMonth(num);}},setDate:function(d){return this.setTime(this.getTime()-(this.getDate()-d)*864e5);}});} +Ext.util.MixedCollection=function(allowFunctions,keyFn){this.items=[];this.map={};this.keys=[];this.length=0;this.addEvents('clear','add','replace','remove','sort');this.allowFunctions=allowFunctions===true;if(keyFn){this.getKey=keyFn;} +Ext.util.MixedCollection.superclass.constructor.call(this);};Ext.extend(Ext.util.MixedCollection,Ext.util.Observable,{allowFunctions:false,add:function(key,o){if(arguments.length==1){o=arguments[0];key=this.getKey(o);} +if(typeof key!='undefined'&&key!==null){var old=this.map[key];if(typeof old!='undefined'){return this.replace(key,o);} +this.map[key]=o;} +this.length++;this.items.push(o);this.keys.push(key);this.fireEvent('add',this.length-1,o,key);return o;},getKey:function(o){return o.id;},replace:function(key,o){if(arguments.length==1){o=arguments[0];key=this.getKey(o);} +var old=this.map[key];if(typeof key=='undefined'||key===null||typeof old=='undefined'){return this.add(key,o);} +var index=this.indexOfKey(key);this.items[index]=o;this.map[key]=o;this.fireEvent('replace',key,old,o);return o;},addAll:function(objs){if(arguments.length>1||Ext.isArray(objs)){var args=arguments.length>1?arguments:objs;for(var i=0,len=args.length;i<len;i++){this.add(args[i]);}}else{for(var key in objs){if(this.allowFunctions||typeof objs[key]!='function'){this.add(key,objs[key]);}}}},each:function(fn,scope){var items=[].concat(this.items);for(var i=0,len=items.length;i<len;i++){if(fn.call(scope||items[i],items[i],i,len)===false){break;}}},eachKey:function(fn,scope){for(var i=0,len=this.keys.length;i<len;i++){fn.call(scope||window,this.keys[i],this.items[i],i,len);}},find:function(fn,scope){for(var i=0,len=this.items.length;i<len;i++){if(fn.call(scope||window,this.items[i],this.keys[i])){return this.items[i];}} +return null;},insert:function(index,key,o){if(arguments.length==2){o=arguments[1];key=this.getKey(o);} +if(this.containsKey(key)){this.suspendEvents();this.removeKey(key);this.resumeEvents();} +if(index>=this.length){return this.add(key,o);} +this.length++;this.items.splice(index,0,o);if(typeof key!='undefined'&&key!==null){this.map[key]=o;} +this.keys.splice(index,0,key);this.fireEvent('add',index,o,key);return o;},remove:function(o){return this.removeAt(this.indexOf(o));},removeAt:function(index){if(index<this.length&&index>=0){this.length--;var o=this.items[index];this.items.splice(index,1);var key=this.keys[index];if(typeof key!='undefined'){delete this.map[key];} +this.keys.splice(index,1);this.fireEvent('remove',o,key);return o;} +return false;},removeKey:function(key){return this.removeAt(this.indexOfKey(key));},getCount:function(){return this.length;},indexOf:function(o){return this.items.indexOf(o);},indexOfKey:function(key){return this.keys.indexOf(key);},item:function(key){var mk=this.map[key],item=mk!==undefined?mk:(typeof key=='number')?this.items[key]:undefined;return typeof item!='function'||this.allowFunctions?item:null;},itemAt:function(index){return this.items[index];},key:function(key){return this.map[key];},contains:function(o){return this.indexOf(o)!=-1;},containsKey:function(key){return typeof this.map[key]!='undefined';},clear:function(){this.length=0;this.items=[];this.keys=[];this.map={};this.fireEvent('clear');},first:function(){return this.items[0];},last:function(){return this.items[this.length-1];},_sort:function(property,dir,fn){var i,len,dsc=String(dir).toUpperCase()=='DESC'?-1:1,c=[],keys=this.keys,items=this.items;fn=fn||function(a,b){return a-b;};for(i=0,len=items.length;i<len;i++){c[c.length]={key:keys[i],value:items[i],index:i};} +c.sort(function(a,b){var v=fn(a[property],b[property])*dsc;if(v===0){v=(a.index<b.index?-1:1);} +return v;});for(i=0,len=c.length;i<len;i++){items[i]=c[i].value;keys[i]=c[i].key;} +this.fireEvent('sort',this);},sort:function(dir,fn){this._sort('value',dir,fn);},reorder:function(mapping){this.suspendEvents();var items=this.items,index=0,length=items.length,order=[],remaining=[],oldIndex;for(oldIndex in mapping){order[mapping[oldIndex]]=items[oldIndex];} +for(index=0;index<length;index++){if(mapping[index]==undefined){remaining.push(items[index]);}} +for(index=0;index<length;index++){if(order[index]==undefined){order[index]=remaining.shift();}} +this.clear();this.addAll(order);this.resumeEvents();this.fireEvent('sort',this);},keySort:function(dir,fn){this._sort('key',dir,fn||function(a,b){var v1=String(a).toUpperCase(),v2=String(b).toUpperCase();return v1>v2?1:(v1<v2?-1:0);});},getRange:function(start,end){var items=this.items;if(items.length<1){return[];} +start=start||0;end=Math.min(typeof end=='undefined'?this.length-1:end,this.length-1);var i,r=[];if(start<=end){for(i=start;i<=end;i++){r[r.length]=items[i];}}else{for(i=start;i>=end;i--){r[r.length]=items[i];}} +return r;},filter:function(property,value,anyMatch,caseSensitive){if(Ext.isEmpty(value,false)){return this.clone();} +value=this.createValueMatcher(value,anyMatch,caseSensitive);return this.filterBy(function(o){return o&&value.test(o[property]);});},filterBy:function(fn,scope){var r=new Ext.util.MixedCollection();r.getKey=this.getKey;var k=this.keys,it=this.items;for(var i=0,len=it.length;i<len;i++){if(fn.call(scope||this,it[i],k[i])){r.add(k[i],it[i]);}} +return r;},findIndex:function(property,value,start,anyMatch,caseSensitive){if(Ext.isEmpty(value,false)){return-1;} +value=this.createValueMatcher(value,anyMatch,caseSensitive);return this.findIndexBy(function(o){return o&&value.test(o[property]);},null,start);},findIndexBy:function(fn,scope,start){var k=this.keys,it=this.items;for(var i=(start||0),len=it.length;i<len;i++){if(fn.call(scope||this,it[i],k[i])){return i;}} +return-1;},createValueMatcher:function(value,anyMatch,caseSensitive,exactMatch){if(!value.exec){var er=Ext.escapeRe;value=String(value);if(anyMatch===true){value=er(value);}else{value='^'+er(value);if(exactMatch===true){value+='$';}} +value=new RegExp(value,caseSensitive?'':'i');} +return value;},clone:function(){var r=new Ext.util.MixedCollection();var k=this.keys,it=this.items;for(var i=0,len=it.length;i<len;i++){r.add(k[i],it[i]);} +r.getKey=this.getKey;return r;}});Ext.util.MixedCollection.prototype.get=Ext.util.MixedCollection.prototype.item;Ext.AbstractManager=Ext.extend(Object,{typeName:'type',constructor:function(config){Ext.apply(this,config||{});this.all=new Ext.util.MixedCollection();this.types={};},get:function(id){return this.all.get(id);},register:function(item){this.all.add(item);},unregister:function(item){this.all.remove(item);},registerType:function(type,cls){this.types[type]=cls;cls[this.typeName]=type;},isRegistered:function(type){return this.types[type]!==undefined;},create:function(config,defaultType){var type=config[this.typeName]||config.type||defaultType,Constructor=this.types[type];if(Constructor==undefined){throw new Error(String.format("The '{0}' type has not been registered with this manager",type));} +return new Constructor(config);},onAvailable:function(id,fn,scope){var all=this.all;all.on("add",function(index,o){if(o.id==id){fn.call(scope||o,o);all.un("add",fn,scope);}});}});Ext.util.Format=function(){var trimRe=/^\s+|\s+$/g,stripTagsRE=/<\/?[^>]+>/gi,stripScriptsRe=/(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig,nl2brRe=/\r?\n/g;return{ellipsis:function(value,len,word){if(value&&value.length>len){if(word){var vs=value.substr(0,len-2),index=Math.max(vs.lastIndexOf(' '),vs.lastIndexOf('.'),vs.lastIndexOf('!'),vs.lastIndexOf('?'));if(index==-1||index<(len-15)){return value.substr(0,len-3)+"...";}else{return vs.substr(0,index)+"...";}}else{return value.substr(0,len-3)+"...";}} +return value;},undef:function(value){return value!==undefined?value:"";},defaultValue:function(value,defaultValue){return value!==undefined&&value!==''?value:defaultValue;},htmlEncode:function(value){return!value?value:String(value).replace(/&/g,"&").replace(/>/g,">").replace(/</g,"<").replace(/"/g,""");},htmlDecode:function(value){return!value?value:String(value).replace(/>/g,">").replace(/</g,"<").replace(/"/g,'"').replace(/&/g,"&");},trim:function(value){return String(value).replace(trimRe,"");},substr:function(value,start,length){return String(value).substr(start,length);},lowercase:function(value){return String(value).toLowerCase();},uppercase:function(value){return String(value).toUpperCase();},capitalize:function(value){return!value?value:value.charAt(0).toUpperCase()+value.substr(1).toLowerCase();},call:function(value,fn){if(arguments.length>2){var args=Array.prototype.slice.call(arguments,2);args.unshift(value);return eval(fn).apply(window,args);}else{return eval(fn).call(window,value);}},usMoney:function(v){v=(Math.round((v-0)*100))/100;v=(v==Math.floor(v))?v+".00":((v*10==Math.floor(v*10))?v+"0":v);v=String(v);var ps=v.split('.'),whole=ps[0],sub=ps[1]?'.'+ps[1]:'.00',r=/(\d+)(\d{3})/;while(r.test(whole)){whole=whole.replace(r,'$1'+','+'$2');} +v=whole+sub;if(v.charAt(0)=='-'){return'-$'+v.substr(1);} +return"$"+v;},date:function(v,format){if(!v){return"";} +if(!Ext.isDate(v)){v=new Date(Date.parse(v));} +return v.dateFormat(format||"m/d/Y");},dateRenderer:function(format){return function(v){return Ext.util.Format.date(v,format);};},stripTags:function(v){return!v?v:String(v).replace(stripTagsRE,"");},stripScripts:function(v){return!v?v:String(v).replace(stripScriptsRe,"");},fileSize:function(size){if(size<1024){return size+" bytes";}else if(size<1048576){return(Math.round(((size*10)/1024))/10)+" KB";}else{return(Math.round(((size*10)/1048576))/10)+" MB";}},math:function(){var fns={};return function(v,a){if(!fns[a]){fns[a]=new Function('v','return v '+a+';');} +return fns[a](v);};}(),round:function(value,precision){var result=Number(value);if(typeof precision=='number'){precision=Math.pow(10,precision);result=Math.round(value*precision)/precision;} +return result;},number:function(v,format){if(!format){return v;} +v=Ext.num(v,NaN);if(isNaN(v)){return'';} +var comma=',',dec='.',i18n=false,neg=v<0;v=Math.abs(v);if(format.substr(format.length-2)=='/i'){format=format.substr(0,format.length-2);i18n=true;comma='.';dec=',';} +var hasComma=format.indexOf(comma)!=-1,psplit=(i18n?format.replace(/[^\d\,]/g,''):format.replace(/[^\d\.]/g,'')).split(dec);if(1<psplit.length){v=v.toFixed(psplit[1].length);}else if(2<psplit.length){throw('NumberFormatException: invalid format, formats should have no more than 1 period: '+format);}else{v=v.toFixed(0);} +var fnum=v.toString();psplit=fnum.split('.');if(hasComma){var cnum=psplit[0],parr=[],j=cnum.length,m=Math.floor(j/3),n=cnum.length%3||3,i;for(i=0;i<j;i+=n){if(i!=0){n=3;} +parr[parr.length]=cnum.substr(i,n);m-=1;} +fnum=parr.join(comma);if(psplit[1]){fnum+=dec+psplit[1];}}else{if(psplit[1]){fnum=psplit[0]+dec+psplit[1];}} +return(neg?'-':'')+format.replace(/[\d,?\.?]+/,fnum);},numberRenderer:function(format){return function(v){return Ext.util.Format.number(v,format);};},plural:function(v,s,p){return v+' '+(v==1?s:(p?p:s+'s'));},nl2br:function(v){return Ext.isEmpty(v)?'':v.replace(nl2brRe,'<br/>');}};}();Ext.XTemplate=function(){Ext.XTemplate.superclass.constructor.apply(this,arguments);var me=this,s=me.html,re=/<tpl\b[^>]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/,nameRe=/^<tpl\b[^>]*?for="(.*?)"/,ifRe=/^<tpl\b[^>]*?if="(.*?)"/,execRe=/^<tpl\b[^>]*?exec="(.*?)"/,m,id=0,tpls=[],VALUES='values',PARENT='parent',XINDEX='xindex',XCOUNT='xcount',RETURN='return ',WITHVALUES='with(values){ ';s=['<tpl>',s,'</tpl>'].join('');while((m=s.match(re))){var m2=m[0].match(nameRe),m3=m[0].match(ifRe),m4=m[0].match(execRe),exp=null,fn=null,exec=null,name=m2&&m2[1]?m2[1]:'';if(m3){exp=m3&&m3[1]?m3[1]:null;if(exp){fn=new Function(VALUES,PARENT,XINDEX,XCOUNT,WITHVALUES+RETURN+(Ext.util.Format.htmlDecode(exp))+'; }');}} +if(m4){exp=m4&&m4[1]?m4[1]:null;if(exp){exec=new Function(VALUES,PARENT,XINDEX,XCOUNT,WITHVALUES+(Ext.util.Format.htmlDecode(exp))+'; }');}} +if(name){switch(name){case'.':name=new Function(VALUES,PARENT,WITHVALUES+RETURN+VALUES+'; }');break;case'..':name=new Function(VALUES,PARENT,WITHVALUES+RETURN+PARENT+'; }');break;default:name=new Function(VALUES,PARENT,WITHVALUES+RETURN+name+'; }');}} +tpls.push({id:id,target:name,exec:exec,test:fn,body:m[1]||''});s=s.replace(m[0],'{xtpl'+id+'}');++id;} +for(var i=tpls.length-1;i>=0;--i){me.compileTpl(tpls[i]);} +me.master=tpls[tpls.length-1];me.tpls=tpls;};Ext.extend(Ext.XTemplate,Ext.Template,{re:/\{([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\\]\s?[\d\.\+\-\*\\\(\)]+)?\}/g,codeRe:/\{\[((?:\\\]|.|\n)*?)\]\}/g,applySubTemplate:function(id,values,parent,xindex,xcount){var me=this,len,t=me.tpls[id],vs,buf=[];if((t.test&&!t.test.call(me,values,parent,xindex,xcount))||(t.exec&&t.exec.call(me,values,parent,xindex,xcount))){return'';} +vs=t.target?t.target.call(me,values,parent):values;len=vs.length;parent=t.target?values:parent;if(t.target&&Ext.isArray(vs)){for(var i=0,len=vs.length;i<len;i++){buf[buf.length]=t.compiled.call(me,vs[i],parent,i+1,len);} +return buf.join('');} +return t.compiled.call(me,vs,parent,xindex,xcount);},compileTpl:function(tpl){var fm=Ext.util.Format,useF=this.disableFormats!==true,sep=Ext.isGecko?"+":",",body;function fn(m,name,format,args,math){if(name.substr(0,4)=='xtpl'){return"'"+sep+'this.applySubTemplate('+name.substr(4)+', values, parent, xindex, xcount)'+sep+"'";} +var v;if(name==='.'){v='values';}else if(name==='#'){v='xindex';}else if(name.indexOf('.')!=-1){v=name;}else{v="values['"+name+"']";} +if(math){v='('+v+math+')';} +if(format&&useF){args=args?','+args:"";if(format.substr(0,5)!="this."){format="fm."+format+'(';}else{format='this.call("'+format.substr(5)+'", ';args=", values";}}else{args='';format="("+v+" === undefined ? '' : ";} +return"'"+sep+format+v+args+")"+sep+"'";} +function codeFn(m,code){return"'"+sep+'('+code.replace(/\\'/g,"'")+')'+sep+"'";} +if(Ext.isGecko){body="tpl.compiled = function(values, parent, xindex, xcount){ return '"+ +tpl.body.replace(/(\r\n|\n)/g,'\\n').replace(/'/g,"\\'").replace(this.re,fn).replace(this.codeRe,codeFn)+"';};";}else{body=["tpl.compiled = function(values, parent, xindex, xcount){ return ['"];body.push(tpl.body.replace(/(\r\n|\n)/g,'\\n').replace(/'/g,"\\'").replace(this.re,fn).replace(this.codeRe,codeFn));body.push("'].join('');};");body=body.join('');} +eval(body);return this;},applyTemplate:function(values){return this.master.compiled.call(this,values,{},1,1);},compile:function(){return this;}});Ext.XTemplate.prototype.apply=Ext.XTemplate.prototype.applyTemplate;Ext.XTemplate.from=function(el){el=Ext.getDom(el);return new Ext.XTemplate(el.value||el.innerHTML);};Ext.util.CSS=function(){var rules=null;var doc=document;var camelRe=/(-[a-z])/gi;var camelFn=function(m,a){return a.charAt(1).toUpperCase();};return{createStyleSheet:function(cssText,id){var ss;var head=doc.getElementsByTagName("head")[0];var rules=doc.createElement("style");rules.setAttribute("type","text/css");if(id){rules.setAttribute("id",id);} +if(Ext.isIE){head.appendChild(rules);ss=rules.styleSheet;ss.cssText=cssText;}else{try{rules.appendChild(doc.createTextNode(cssText));}catch(e){rules.cssText=cssText;} +head.appendChild(rules);ss=rules.styleSheet?rules.styleSheet:(rules.sheet||doc.styleSheets[doc.styleSheets.length-1]);} +this.cacheStyleSheet(ss);return ss;},removeStyleSheet:function(id){var existing=doc.getElementById(id);if(existing){existing.parentNode.removeChild(existing);}},swapStyleSheet:function(id,url){this.removeStyleSheet(id);var ss=doc.createElement("link");ss.setAttribute("rel","stylesheet");ss.setAttribute("type","text/css");ss.setAttribute("id",id);ss.setAttribute("href",url);doc.getElementsByTagName("head")[0].appendChild(ss);},refreshCache:function(){return this.getRules(true);},cacheStyleSheet:function(ss){if(!rules){rules={};} +try{var ssRules=ss.cssRules||ss.rules;for(var j=ssRules.length-1;j>=0;--j){rules[ssRules[j].selectorText.toLowerCase()]=ssRules[j];}}catch(e){}},getRules:function(refreshCache){if(rules===null||refreshCache){rules={};var ds=doc.styleSheets;for(var i=0,len=ds.length;i<len;i++){try{this.cacheStyleSheet(ds[i]);}catch(e){}}} +return rules;},getRule:function(selector,refreshCache){var rs=this.getRules(refreshCache);if(!Ext.isArray(selector)){return rs[selector.toLowerCase()];} +for(var i=0;i<selector.length;i++){if(rs[selector[i]]){return rs[selector[i].toLowerCase()];}} +return null;},updateRule:function(selector,property,value){if(!Ext.isArray(selector)){var rule=this.getRule(selector);if(rule){rule.style[property.replace(camelRe,camelFn)]=value;return true;}}else{for(var i=0;i<selector.length;i++){if(this.updateRule(selector[i],property,value)){return true;}}} +return false;}};}();Ext.util.ClickRepeater=Ext.extend(Ext.util.Observable,{constructor:function(el,config){this.el=Ext.get(el);this.el.unselectable();Ext.apply(this,config);this.addEvents("mousedown","click","mouseup");if(!this.disabled){this.disabled=true;this.enable();} +if(this.handler){this.on("click",this.handler,this.scope||this);} +Ext.util.ClickRepeater.superclass.constructor.call(this);},interval:20,delay:250,preventDefault:true,stopDefault:false,timer:0,enable:function(){if(this.disabled){this.el.on('mousedown',this.handleMouseDown,this);if(Ext.isIE){this.el.on('dblclick',this.handleDblClick,this);} +if(this.preventDefault||this.stopDefault){this.el.on('click',this.eventOptions,this);}} +this.disabled=false;},disable:function(force){if(force||!this.disabled){clearTimeout(this.timer);if(this.pressClass){this.el.removeClass(this.pressClass);} +Ext.getDoc().un('mouseup',this.handleMouseUp,this);this.el.removeAllListeners();} +this.disabled=true;},setDisabled:function(disabled){this[disabled?'disable':'enable']();},eventOptions:function(e){if(this.preventDefault){e.preventDefault();} +if(this.stopDefault){e.stopEvent();}},destroy:function(){this.disable(true);Ext.destroy(this.el);this.purgeListeners();},handleDblClick:function(e){clearTimeout(this.timer);this.el.blur();this.fireEvent("mousedown",this,e);this.fireEvent("click",this,e);},handleMouseDown:function(e){clearTimeout(this.timer);this.el.blur();if(this.pressClass){this.el.addClass(this.pressClass);} +this.mousedownTime=new Date();Ext.getDoc().on("mouseup",this.handleMouseUp,this);this.el.on("mouseout",this.handleMouseOut,this);this.fireEvent("mousedown",this,e);this.fireEvent("click",this,e);if(this.accelerate){this.delay=400;} +this.timer=this.click.defer(this.delay||this.interval,this,[e]);},click:function(e){this.fireEvent("click",this,e);this.timer=this.click.defer(this.accelerate?this.easeOutExpo(this.mousedownTime.getElapsed(),400,-390,12000):this.interval,this,[e]);},easeOutExpo:function(t,b,c,d){return(t==d)?b+c:c*(-Math.pow(2,-10*t/d)+1)+b;},handleMouseOut:function(){clearTimeout(this.timer);if(this.pressClass){this.el.removeClass(this.pressClass);} +this.el.on("mouseover",this.handleMouseReturn,this);},handleMouseReturn:function(){this.el.un("mouseover",this.handleMouseReturn,this);if(this.pressClass){this.el.addClass(this.pressClass);} +this.click();},handleMouseUp:function(e){clearTimeout(this.timer);this.el.un("mouseover",this.handleMouseReturn,this);this.el.un("mouseout",this.handleMouseOut,this);Ext.getDoc().un("mouseup",this.handleMouseUp,this);this.el.removeClass(this.pressClass);this.fireEvent("mouseup",this,e);}});Ext.KeyNav=function(el,config){this.el=Ext.get(el);Ext.apply(this,config);if(!this.disabled){this.disabled=true;this.enable();}};Ext.KeyNav.prototype={disabled:false,defaultEventAction:"stopEvent",forceKeyDown:false,relay:function(e){var k=e.getKey(),h=this.keyToHandler[k];if(h&&this[h]){if(this.doRelay(e,this[h],h)!==true){e[this.defaultEventAction]();}}},doRelay:function(e,h,hname){return h.call(this.scope||this,e,hname);},enter:false,left:false,right:false,up:false,down:false,tab:false,esc:false,pageUp:false,pageDown:false,del:false,home:false,end:false,keyToHandler:{37:"left",39:"right",38:"up",40:"down",33:"pageUp",34:"pageDown",46:"del",36:"home",35:"end",13:"enter",27:"esc",9:"tab"},stopKeyUp:function(e){var k=e.getKey();if(k>=37&&k<=40){e.stopEvent();}},destroy:function(){this.disable();},enable:function(){if(this.disabled){if(Ext.isSafari2){this.el.on('keyup',this.stopKeyUp,this);} +this.el.on(this.isKeydown()?'keydown':'keypress',this.relay,this);this.disabled=false;}},disable:function(){if(!this.disabled){if(Ext.isSafari2){this.el.un('keyup',this.stopKeyUp,this);} +this.el.un(this.isKeydown()?'keydown':'keypress',this.relay,this);this.disabled=true;}},setDisabled:function(disabled){this[disabled?"disable":"enable"]();},isKeydown:function(){return this.forceKeyDown||Ext.EventManager.useKeydown;}};Ext.KeyMap=function(el,config,eventName){this.el=Ext.get(el);this.eventName=eventName||"keydown";this.bindings=[];if(config){this.addBinding(config);} +this.enable();};Ext.KeyMap.prototype={stopEvent:false,addBinding:function(config){if(Ext.isArray(config)){Ext.each(config,function(c){this.addBinding(c);},this);return;} +var keyCode=config.key,fn=config.fn||config.handler,scope=config.scope;if(config.stopEvent){this.stopEvent=config.stopEvent;} +if(typeof keyCode=="string"){var ks=[];var keyString=keyCode.toUpperCase();for(var j=0,len=keyString.length;j<len;j++){ks.push(keyString.charCodeAt(j));} +keyCode=ks;} +var keyArray=Ext.isArray(keyCode);var handler=function(e){if(this.checkModifiers(config,e)){var k=e.getKey();if(keyArray){for(var i=0,len=keyCode.length;i<len;i++){if(keyCode[i]==k){if(this.stopEvent){e.stopEvent();} +fn.call(scope||window,k,e);return;}}}else{if(k==keyCode){if(this.stopEvent){e.stopEvent();} +fn.call(scope||window,k,e);}}}};this.bindings.push(handler);},checkModifiers:function(config,e){var val,key,keys=['shift','ctrl','alt'];for(var i=0,len=keys.length;i<len;++i){key=keys[i];val=config[key];if(!(val===undefined||(val===e[key+'Key']))){return false;}} +return true;},on:function(key,fn,scope){var keyCode,shift,ctrl,alt;if(typeof key=="object"&&!Ext.isArray(key)){keyCode=key.key;shift=key.shift;ctrl=key.ctrl;alt=key.alt;}else{keyCode=key;} +this.addBinding({key:keyCode,shift:shift,ctrl:ctrl,alt:alt,fn:fn,scope:scope});},handleKeyDown:function(e){if(this.enabled){var b=this.bindings;for(var i=0,len=b.length;i<len;i++){b[i].call(this,e);}}},isEnabled:function(){return this.enabled;},enable:function(){if(!this.enabled){this.el.on(this.eventName,this.handleKeyDown,this);this.enabled=true;}},disable:function(){if(this.enabled){this.el.removeListener(this.eventName,this.handleKeyDown,this);this.enabled=false;}},setDisabled:function(disabled){this[disabled?"disable":"enable"]();}};Ext.util.TextMetrics=function(){var shared;return{measure:function(el,text,fixedWidth){if(!shared){shared=Ext.util.TextMetrics.Instance(el,fixedWidth);} +shared.bind(el);shared.setFixedWidth(fixedWidth||'auto');return shared.getSize(text);},createInstance:function(el,fixedWidth){return Ext.util.TextMetrics.Instance(el,fixedWidth);}};}();Ext.util.TextMetrics.Instance=function(bindTo,fixedWidth){var ml=new Ext.Element(document.createElement('div'));document.body.appendChild(ml.dom);ml.position('absolute');ml.setLeftTop(-1000,-1000);ml.hide();if(fixedWidth){ml.setWidth(fixedWidth);} +var instance={getSize:function(text){ml.update(text);var s=ml.getSize();ml.update('');return s;},bind:function(el){ml.setStyle(Ext.fly(el).getStyles('font-size','font-style','font-weight','font-family','line-height','text-transform','letter-spacing'));},setFixedWidth:function(width){ml.setWidth(width);},getWidth:function(text){ml.dom.style.width='auto';return this.getSize(text).width;},getHeight:function(text){return this.getSize(text).height;}};instance.bind(bindTo);return instance;};Ext.Element.addMethods({getTextWidth:function(text,min,max){return(Ext.util.TextMetrics.measure(this.dom,Ext.value(text,this.dom.innerHTML,true)).width).constrain(min||0,max||1000000);}});Ext.util.Cookies={set:function(name,value){var argv=arguments;var argc=arguments.length;var expires=(argc>2)?argv[2]:null;var path=(argc>3)?argv[3]:'/';var domain=(argc>4)?argv[4]:null;var secure=(argc>5)?argv[5]:false;document.cookie=name+"="+escape(value)+((expires===null)?"":("; expires="+expires.toGMTString()))+((path===null)?"":("; path="+path))+((domain===null)?"":("; domain="+domain))+((secure===true)?"; secure":"");},get:function(name){var arg=name+"=";var alen=arg.length;var clen=document.cookie.length;var i=0;var j=0;while(i<clen){j=i+alen;if(document.cookie.substring(i,j)==arg){return Ext.util.Cookies.getCookieVal(j);} +i=document.cookie.indexOf(" ",i)+1;if(i===0){break;}} +return null;},clear:function(name){if(Ext.util.Cookies.get(name)){document.cookie=name+"="+"; expires=Thu, 01-Jan-70 00:00:01 GMT";}},getCookieVal:function(offset){var endstr=document.cookie.indexOf(";",offset);if(endstr==-1){endstr=document.cookie.length;} +return unescape(document.cookie.substring(offset,endstr));}};Ext.handleError=function(e){throw e;};Ext.Error=function(message){this.message=(this.lang[message])?this.lang[message]:message;};Ext.Error.prototype=new Error();Ext.apply(Ext.Error.prototype,{lang:{},name:'Ext.Error',getName:function(){return this.name;},getMessage:function(){return this.message;},toJson:function(){return Ext.encode(this);}});Ext.ComponentMgr=function(){var all=new Ext.util.MixedCollection();var types={};var ptypes={};return{register:function(c){all.add(c);},unregister:function(c){all.remove(c);},get:function(id){return all.get(id);},onAvailable:function(id,fn,scope){all.on("add",function(index,o){if(o.id==id){fn.call(scope||o,o);all.un("add",fn,scope);}});},all:all,types:types,ptypes:ptypes,isRegistered:function(xtype){return types[xtype]!==undefined;},isPluginRegistered:function(ptype){return ptypes[ptype]!==undefined;},registerType:function(xtype,cls){types[xtype]=cls;cls.xtype=xtype;},create:function(config,defaultType){return config.render?config:new types[config.xtype||defaultType](config);},registerPlugin:function(ptype,cls){ptypes[ptype]=cls;cls.ptype=ptype;},createPlugin:function(config,defaultType){var PluginCls=ptypes[config.ptype||defaultType];if(PluginCls.init){return PluginCls;}else{return new PluginCls(config);}}};}();Ext.reg=Ext.ComponentMgr.registerType;Ext.preg=Ext.ComponentMgr.registerPlugin;Ext.create=Ext.ComponentMgr.create;Ext.Component=function(config){config=config||{};if(config.initialConfig){if(config.isAction){this.baseAction=config;} +config=config.initialConfig;}else if(config.tagName||config.dom||Ext.isString(config)){config={applyTo:config,id:config.id||config};} +this.initialConfig=config;Ext.apply(this,config);this.addEvents('added','disable','enable','beforeshow','show','beforehide','hide','removed','beforerender','render','afterrender','beforedestroy','destroy','beforestaterestore','staterestore','beforestatesave','statesave');this.getId();Ext.ComponentMgr.register(this);Ext.Component.superclass.constructor.call(this);if(this.baseAction){this.baseAction.addComponent(this);} +this.initComponent();if(this.plugins){if(Ext.isArray(this.plugins)){for(var i=0,len=this.plugins.length;i<len;i++){this.plugins[i]=this.initPlugin(this.plugins[i]);}}else{this.plugins=this.initPlugin(this.plugins);}} +if(this.stateful!==false){this.initState();} +if(this.applyTo){this.applyToMarkup(this.applyTo);delete this.applyTo;}else if(this.renderTo){this.render(this.renderTo);delete this.renderTo;}};Ext.Component.AUTO_ID=1000;Ext.extend(Ext.Component,Ext.util.Observable,{disabled:false,hidden:false,autoEl:'div',disabledClass:'x-item-disabled',allowDomMove:true,autoShow:false,hideMode:'display',hideParent:false,rendered:false,tplWriteMode:'overwrite',bubbleEvents:[],ctype:'Ext.Component',actionMode:'el',getActionEl:function(){return this[this.actionMode];},initPlugin:function(p){if(p.ptype&&!Ext.isFunction(p.init)){p=Ext.ComponentMgr.createPlugin(p);}else if(Ext.isString(p)){p=Ext.ComponentMgr.createPlugin({ptype:p});} +p.init(this);return p;},initComponent:function(){if(this.listeners){this.on(this.listeners);delete this.listeners;} +this.enableBubble(this.bubbleEvents);},render:function(container,position){if(!this.rendered&&this.fireEvent('beforerender',this)!==false){if(!container&&this.el){this.el=Ext.get(this.el);container=this.el.dom.parentNode;this.allowDomMove=false;} +this.container=Ext.get(container);if(this.ctCls){this.container.addClass(this.ctCls);} +this.rendered=true;if(position!==undefined){if(Ext.isNumber(position)){position=this.container.dom.childNodes[position];}else{position=Ext.getDom(position);}} +this.onRender(this.container,position||null);if(this.autoShow){this.el.removeClass(['x-hidden','x-hide-'+this.hideMode]);} +if(this.cls){this.el.addClass(this.cls);delete this.cls;} +if(this.style){this.el.applyStyles(this.style);delete this.style;} +if(this.overCls){this.el.addClassOnOver(this.overCls);} +this.fireEvent('render',this);var contentTarget=this.getContentTarget();if(this.html){contentTarget.update(Ext.DomHelper.markup(this.html));delete this.html;} +if(this.contentEl){var ce=Ext.getDom(this.contentEl);Ext.fly(ce).removeClass(['x-hidden','x-hide-display']);contentTarget.appendChild(ce);} +if(this.tpl){if(!this.tpl.compile){this.tpl=new Ext.XTemplate(this.tpl);} +if(this.data){this.tpl[this.tplWriteMode](contentTarget,this.data);delete this.data;}} +this.afterRender(this.container);if(this.hidden){this.doHide();} +if(this.disabled){this.disable(true);} +if(this.stateful!==false){this.initStateEvents();} +this.fireEvent('afterrender',this);} +return this;},update:function(htmlOrData,loadScripts,cb){var contentTarget=this.getContentTarget();if(this.tpl&&typeof htmlOrData!=="string"){this.tpl[this.tplWriteMode](contentTarget,htmlOrData||{});}else{var html=Ext.isObject(htmlOrData)?Ext.DomHelper.markup(htmlOrData):htmlOrData;contentTarget.update(html,loadScripts,cb);}},onAdded:function(container,pos){this.ownerCt=container;this.initRef();this.fireEvent('added',this,container,pos);},onRemoved:function(){this.removeRef();this.fireEvent('removed',this,this.ownerCt);delete this.ownerCt;},initRef:function(){if(this.ref&&!this.refOwner){var levels=this.ref.split('/'),last=levels.length,i=0,t=this;while(t&&i<last){t=t.ownerCt;++i;} +if(t){t[this.refName=levels[--i]]=this;this.refOwner=t;}}},removeRef:function(){if(this.refOwner&&this.refName){delete this.refOwner[this.refName];delete this.refOwner;}},initState:function(){if(Ext.state.Manager){var id=this.getStateId();if(id){var state=Ext.state.Manager.get(id);if(state){if(this.fireEvent('beforestaterestore',this,state)!==false){this.applyState(Ext.apply({},state));this.fireEvent('staterestore',this,state);}}}}},getStateId:function(){return this.stateId||((/^(ext-comp-|ext-gen)/).test(String(this.id))?null:this.id);},initStateEvents:function(){if(this.stateEvents){for(var i=0,e;e=this.stateEvents[i];i++){this.on(e,this.saveState,this,{delay:100});}}},applyState:function(state){if(state){Ext.apply(this,state);}},getState:function(){return null;},saveState:function(){if(Ext.state.Manager&&this.stateful!==false){var id=this.getStateId();if(id){var state=this.getState();if(this.fireEvent('beforestatesave',this,state)!==false){Ext.state.Manager.set(id,state);this.fireEvent('statesave',this,state);}}}},applyToMarkup:function(el){this.allowDomMove=false;this.el=Ext.get(el);this.render(this.el.dom.parentNode);},addClass:function(cls){if(this.el){this.el.addClass(cls);}else{this.cls=this.cls?this.cls+' '+cls:cls;} +return this;},removeClass:function(cls){if(this.el){this.el.removeClass(cls);}else if(this.cls){this.cls=this.cls.split(' ').remove(cls).join(' ');} +return this;},onRender:function(ct,position){if(!this.el&&this.autoEl){if(Ext.isString(this.autoEl)){this.el=document.createElement(this.autoEl);}else{var div=document.createElement('div');Ext.DomHelper.overwrite(div,this.autoEl);this.el=div.firstChild;} +if(!this.el.id){this.el.id=this.getId();}} +if(this.el){this.el=Ext.get(this.el);if(this.allowDomMove!==false){ct.dom.insertBefore(this.el.dom,position);if(div){Ext.removeNode(div);div=null;}}}},getAutoCreate:function(){var cfg=Ext.isObject(this.autoCreate)?this.autoCreate:Ext.apply({},this.defaultAutoCreate);if(this.id&&!cfg.id){cfg.id=this.id;} +return cfg;},afterRender:Ext.emptyFn,destroy:function(){if(!this.isDestroyed){if(this.fireEvent('beforedestroy',this)!==false){this.destroying=true;this.beforeDestroy();if(this.ownerCt&&this.ownerCt.remove){this.ownerCt.remove(this,false);} +if(this.rendered){this.el.remove();if(this.actionMode=='container'||this.removeMode=='container'){this.container.remove();}} +if(this.focusTask&&this.focusTask.cancel){this.focusTask.cancel();} +this.onDestroy();Ext.ComponentMgr.unregister(this);this.fireEvent('destroy',this);this.purgeListeners();this.destroying=false;this.isDestroyed=true;}}},deleteMembers:function(){var args=arguments;for(var i=0,len=args.length;i<len;++i){delete this[args[i]];}},beforeDestroy:Ext.emptyFn,onDestroy:Ext.emptyFn,getEl:function(){return this.el;},getContentTarget:function(){return this.el;},getId:function(){return this.id||(this.id='ext-comp-'+(++Ext.Component.AUTO_ID));},getItemId:function(){return this.itemId||this.getId();},focus:function(selectText,delay){if(delay){this.focusTask=new Ext.util.DelayedTask(this.focus,this,[selectText,false]);this.focusTask.delay(Ext.isNumber(delay)?delay:10);return this;} +if(this.rendered&&!this.isDestroyed){this.el.focus();if(selectText===true){this.el.dom.select();}} +return this;},blur:function(){if(this.rendered){this.el.blur();} +return this;},disable:function(silent){if(this.rendered){this.onDisable();} +this.disabled=true;if(silent!==true){this.fireEvent('disable',this);} +return this;},onDisable:function(){this.getActionEl().addClass(this.disabledClass);this.el.dom.disabled=true;},enable:function(){if(this.rendered){this.onEnable();} +this.disabled=false;this.fireEvent('enable',this);return this;},onEnable:function(){this.getActionEl().removeClass(this.disabledClass);this.el.dom.disabled=false;},setDisabled:function(disabled){return this[disabled?'disable':'enable']();},show:function(){if(this.fireEvent('beforeshow',this)!==false){this.hidden=false;if(this.autoRender){this.render(Ext.isBoolean(this.autoRender)?Ext.getBody():this.autoRender);} +if(this.rendered){this.onShow();} +this.fireEvent('show',this);} +return this;},onShow:function(){this.getVisibilityEl().removeClass('x-hide-'+this.hideMode);},hide:function(){if(this.fireEvent('beforehide',this)!==false){this.doHide();this.fireEvent('hide',this);} +return this;},doHide:function(){this.hidden=true;if(this.rendered){this.onHide();}},onHide:function(){this.getVisibilityEl().addClass('x-hide-'+this.hideMode);},getVisibilityEl:function(){return this.hideParent?this.container:this.getActionEl();},setVisible:function(visible){return this[visible?'show':'hide']();},isVisible:function(){return this.rendered&&this.getVisibilityEl().isVisible();},cloneConfig:function(overrides){overrides=overrides||{};var id=overrides.id||Ext.id();var cfg=Ext.applyIf(overrides,this.initialConfig);cfg.id=id;return new this.constructor(cfg);},getXType:function(){return this.constructor.xtype;},isXType:function(xtype,shallow){if(Ext.isFunction(xtype)){xtype=xtype.xtype;}else if(Ext.isObject(xtype)){xtype=xtype.constructor.xtype;} +return!shallow?('/'+this.getXTypes()+'/').indexOf('/'+xtype+'/')!=-1:this.constructor.xtype==xtype;},getXTypes:function(){var tc=this.constructor;if(!tc.xtypes){var c=[],sc=this;while(sc&&sc.constructor.xtype){c.unshift(sc.constructor.xtype);sc=sc.constructor.superclass;} +tc.xtypeChain=c;tc.xtypes=c.join('/');} +return tc.xtypes;},findParentBy:function(fn){for(var p=this.ownerCt;(p!=null)&&!fn(p,this);p=p.ownerCt);return p||null;},findParentByType:function(xtype,shallow){return this.findParentBy(function(c){return c.isXType(xtype,shallow);});},bubble:function(fn,scope,args){var p=this;while(p){if(fn.apply(scope||p,args||[p])===false){break;} +p=p.ownerCt;} +return this;},getPositionEl:function(){return this.positionEl||this.el;},purgeListeners:function(){Ext.Component.superclass.purgeListeners.call(this);if(this.mons){this.on('beforedestroy',this.clearMons,this,{single:true});}},clearMons:function(){Ext.each(this.mons,function(m){m.item.un(m.ename,m.fn,m.scope);},this);this.mons=[];},createMons:function(){if(!this.mons){this.mons=[];this.on('beforedestroy',this.clearMons,this,{single:true});}},mon:function(item,ename,fn,scope,opt){this.createMons();if(Ext.isObject(ename)){var propRe=/^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/;var o=ename;for(var e in o){if(propRe.test(e)){continue;} +if(Ext.isFunction(o[e])){this.mons.push({item:item,ename:e,fn:o[e],scope:o.scope});item.on(e,o[e],o.scope,o);}else{this.mons.push({item:item,ename:e,fn:o[e],scope:o.scope});item.on(e,o[e]);}} +return;} +this.mons.push({item:item,ename:ename,fn:fn,scope:scope});item.on(ename,fn,scope,opt);},mun:function(item,ename,fn,scope){var found,mon;this.createMons();for(var i=0,len=this.mons.length;i<len;++i){mon=this.mons[i];if(item===mon.item&&ename==mon.ename&&fn===mon.fn&&scope===mon.scope){this.mons.splice(i,1);item.un(ename,fn,scope);found=true;break;}} +return found;},nextSibling:function(){if(this.ownerCt){var index=this.ownerCt.items.indexOf(this);if(index!=-1&&index+1<this.ownerCt.items.getCount()){return this.ownerCt.items.itemAt(index+1);}} +return null;},previousSibling:function(){if(this.ownerCt){var index=this.ownerCt.items.indexOf(this);if(index>0){return this.ownerCt.items.itemAt(index-1);}} +return null;},getBubbleTarget:function(){return this.ownerCt;}});Ext.reg('component',Ext.Component);Ext.Action=Ext.extend(Object,{constructor:function(config){this.initialConfig=config;this.itemId=config.itemId=(config.itemId||config.id||Ext.id());this.items=[];},isAction:true,setText:function(text){this.initialConfig.text=text;this.callEach('setText',[text]);},getText:function(){return this.initialConfig.text;},setIconClass:function(cls){this.initialConfig.iconCls=cls;this.callEach('setIconClass',[cls]);},getIconClass:function(){return this.initialConfig.iconCls;},setDisabled:function(v){this.initialConfig.disabled=v;this.callEach('setDisabled',[v]);},enable:function(){this.setDisabled(false);},disable:function(){this.setDisabled(true);},isDisabled:function(){return this.initialConfig.disabled;},setHidden:function(v){this.initialConfig.hidden=v;this.callEach('setVisible',[!v]);},show:function(){this.setHidden(false);},hide:function(){this.setHidden(true);},isHidden:function(){return this.initialConfig.hidden;},setHandler:function(fn,scope){this.initialConfig.handler=fn;this.initialConfig.scope=scope;this.callEach('setHandler',[fn,scope]);},each:function(fn,scope){Ext.each(this.items,fn,scope);},callEach:function(fnName,args){var cs=this.items;for(var i=0,len=cs.length;i<len;i++){cs[i][fnName].apply(cs[i],args);}},addComponent:function(comp){this.items.push(comp);comp.on('destroy',this.removeComponent,this);},removeComponent:function(comp){this.items.remove(comp);},execute:function(){this.initialConfig.handler.apply(this.initialConfig.scope||window,arguments);}});(function(){Ext.Layer=function(config,existingEl){config=config||{};var dh=Ext.DomHelper,cp=config.parentEl,pel=cp?Ext.getDom(cp):document.body;if(existingEl){this.dom=Ext.getDom(existingEl);} +if(!this.dom){var o=config.dh||{tag:'div',cls:'x-layer'};this.dom=dh.append(pel,o);} +if(config.cls){this.addClass(config.cls);} +this.constrain=config.constrain!==false;this.setVisibilityMode(Ext.Element.VISIBILITY);if(config.id){this.id=this.dom.id=config.id;}else{this.id=Ext.id(this.dom);} +this.zindex=config.zindex||this.getZIndex();this.position('absolute',this.zindex);if(config.shadow){this.shadowOffset=config.shadowOffset||4;this.shadow=new Ext.Shadow({offset:this.shadowOffset,mode:config.shadow});}else{this.shadowOffset=0;} +this.useShim=config.shim!==false&&Ext.useShims;this.useDisplay=config.useDisplay;this.hide();};var supr=Ext.Element.prototype;var shims=[];Ext.extend(Ext.Layer,Ext.Element,{getZIndex:function(){return this.zindex||parseInt((this.getShim()||this).getStyle('z-index'),10)||11000;},getShim:function(){if(!this.useShim){return null;} +if(this.shim){return this.shim;} +var shim=shims.shift();if(!shim){shim=this.createShim();shim.enableDisplayMode('block');shim.dom.style.display='none';shim.dom.style.visibility='visible';} +var pn=this.dom.parentNode;if(shim.dom.parentNode!=pn){pn.insertBefore(shim.dom,this.dom);} +shim.setStyle('z-index',this.getZIndex()-2);this.shim=shim;return shim;},hideShim:function(){if(this.shim){this.shim.setDisplayed(false);shims.push(this.shim);delete this.shim;}},disableShadow:function(){if(this.shadow){this.shadowDisabled=true;this.shadow.hide();this.lastShadowOffset=this.shadowOffset;this.shadowOffset=0;}},enableShadow:function(show){if(this.shadow){this.shadowDisabled=false;this.shadowOffset=this.lastShadowOffset;delete this.lastShadowOffset;if(show){this.sync(true);}}},sync:function(doShow){var shadow=this.shadow;if(!this.updating&&this.isVisible()&&(shadow||this.useShim)){var shim=this.getShim(),w=this.getWidth(),h=this.getHeight(),l=this.getLeft(true),t=this.getTop(true);if(shadow&&!this.shadowDisabled){if(doShow&&!shadow.isVisible()){shadow.show(this);}else{shadow.realign(l,t,w,h);} +if(shim){if(doShow){shim.show();} +var shadowAdj=shadow.el.getXY(),shimStyle=shim.dom.style,shadowSize=shadow.el.getSize();shimStyle.left=(shadowAdj[0])+'px';shimStyle.top=(shadowAdj[1])+'px';shimStyle.width=(shadowSize.width)+'px';shimStyle.height=(shadowSize.height)+'px';}}else if(shim){if(doShow){shim.show();} +shim.setSize(w,h);shim.setLeftTop(l,t);}}},destroy:function(){this.hideShim();if(this.shadow){this.shadow.hide();} +this.removeAllListeners();Ext.removeNode(this.dom);delete this.dom;},remove:function(){this.destroy();},beginUpdate:function(){this.updating=true;},endUpdate:function(){this.updating=false;this.sync(true);},hideUnders:function(negOffset){if(this.shadow){this.shadow.hide();} +this.hideShim();},constrainXY:function(){if(this.constrain){var vw=Ext.lib.Dom.getViewWidth(),vh=Ext.lib.Dom.getViewHeight();var s=Ext.getDoc().getScroll();var xy=this.getXY();var x=xy[0],y=xy[1];var so=this.shadowOffset;var w=this.dom.offsetWidth+so,h=this.dom.offsetHeight+so;var moved=false;if((x+w)>vw+s.left){x=vw-w-so;moved=true;} +if((y+h)>vh+s.top){y=vh-h-so;moved=true;} +if(x<s.left){x=s.left;moved=true;} +if(y<s.top){y=s.top;moved=true;} +if(moved){if(this.avoidY){var ay=this.avoidY;if(y<=ay&&(y+h)>=ay){y=ay-h-5;}} +xy=[x,y];this.storeXY(xy);supr.setXY.call(this,xy);this.sync();}} +return this;},getConstrainOffset:function(){return this.shadowOffset;},isVisible:function(){return this.visible;},showAction:function(){this.visible=true;if(this.useDisplay===true){this.setDisplayed('');}else if(this.lastXY){supr.setXY.call(this,this.lastXY);}else if(this.lastLT){supr.setLeftTop.call(this,this.lastLT[0],this.lastLT[1]);}},hideAction:function(){this.visible=false;if(this.useDisplay===true){this.setDisplayed(false);}else{this.setLeftTop(-10000,-10000);}},setVisible:function(v,a,d,c,e){if(v){this.showAction();} +if(a&&v){var cb=function(){this.sync(true);if(c){c();}}.createDelegate(this);supr.setVisible.call(this,true,true,d,cb,e);}else{if(!v){this.hideUnders(true);} +var cb=c;if(a){cb=function(){this.hideAction();if(c){c();}}.createDelegate(this);} +supr.setVisible.call(this,v,a,d,cb,e);if(v){this.sync(true);}else if(!a){this.hideAction();}} +return this;},storeXY:function(xy){delete this.lastLT;this.lastXY=xy;},storeLeftTop:function(left,top){delete this.lastXY;this.lastLT=[left,top];},beforeFx:function(){this.beforeAction();return Ext.Layer.superclass.beforeFx.apply(this,arguments);},afterFx:function(){Ext.Layer.superclass.afterFx.apply(this,arguments);this.sync(this.isVisible());},beforeAction:function(){if(!this.updating&&this.shadow){this.shadow.hide();}},setLeft:function(left){this.storeLeftTop(left,this.getTop(true));supr.setLeft.apply(this,arguments);this.sync();return this;},setTop:function(top){this.storeLeftTop(this.getLeft(true),top);supr.setTop.apply(this,arguments);this.sync();return this;},setLeftTop:function(left,top){this.storeLeftTop(left,top);supr.setLeftTop.apply(this,arguments);this.sync();return this;},setXY:function(xy,a,d,c,e){this.fixDisplay();this.beforeAction();this.storeXY(xy);var cb=this.createCB(c);supr.setXY.call(this,xy,a,d,cb,e);if(!a){cb();} +return this;},createCB:function(c){var el=this;return function(){el.constrainXY();el.sync(true);if(c){c();}};},setX:function(x,a,d,c,e){this.setXY([x,this.getY()],a,d,c,e);return this;},setY:function(y,a,d,c,e){this.setXY([this.getX(),y],a,d,c,e);return this;},setSize:function(w,h,a,d,c,e){this.beforeAction();var cb=this.createCB(c);supr.setSize.call(this,w,h,a,d,cb,e);if(!a){cb();} +return this;},setWidth:function(w,a,d,c,e){this.beforeAction();var cb=this.createCB(c);supr.setWidth.call(this,w,a,d,cb,e);if(!a){cb();} +return this;},setHeight:function(h,a,d,c,e){this.beforeAction();var cb=this.createCB(c);supr.setHeight.call(this,h,a,d,cb,e);if(!a){cb();} +return this;},setBounds:function(x,y,w,h,a,d,c,e){this.beforeAction();var cb=this.createCB(c);if(!a){this.storeXY([x,y]);supr.setXY.call(this,[x,y]);supr.setSize.call(this,w,h,a,d,cb,e);cb();}else{supr.setBounds.call(this,x,y,w,h,a,d,cb,e);} +return this;},setZIndex:function(zindex){this.zindex=zindex;this.setStyle('z-index',zindex+2);if(this.shadow){this.shadow.setZIndex(zindex+1);} +if(this.shim){this.shim.setStyle('z-index',zindex);} +return this;}});})();Ext.Shadow=function(config){Ext.apply(this,config);if(typeof this.mode!="string"){this.mode=this.defaultMode;} +var o=this.offset,a={h:0},rad=Math.floor(this.offset/2);switch(this.mode.toLowerCase()){case"drop":a.w=0;a.l=a.t=o;a.t-=1;if(Ext.isIE){a.l-=this.offset+rad;a.t-=this.offset+rad;a.w-=rad;a.h-=rad;a.t+=1;} +break;case"sides":a.w=(o*2);a.l=-o;a.t=o-1;if(Ext.isIE){a.l-=(this.offset-rad);a.t-=this.offset+rad;a.l+=1;a.w-=(this.offset-rad)*2;a.w-=rad+1;a.h-=1;} +break;case"frame":a.w=a.h=(o*2);a.l=a.t=-o;a.t+=1;a.h-=2;if(Ext.isIE){a.l-=(this.offset-rad);a.t-=(this.offset-rad);a.l+=1;a.w-=(this.offset+rad+1);a.h-=(this.offset+rad);a.h+=1;} +break;};this.adjusts=a;};Ext.Shadow.prototype={offset:4,defaultMode:"drop",show:function(target){target=Ext.get(target);if(!this.el){this.el=Ext.Shadow.Pool.pull();if(this.el.dom.nextSibling!=target.dom){this.el.insertBefore(target);}} +this.el.setStyle("z-index",this.zIndex||parseInt(target.getStyle("z-index"),10)-1);if(Ext.isIE){this.el.dom.style.filter="progid:DXImageTransform.Microsoft.alpha(opacity=50) progid:DXImageTransform.Microsoft.Blur(pixelradius="+(this.offset)+")";} +this.realign(target.getLeft(true),target.getTop(true),target.getWidth(),target.getHeight());this.el.dom.style.display="block";},isVisible:function(){return this.el?true:false;},realign:function(l,t,w,h){if(!this.el){return;} +var a=this.adjusts,d=this.el.dom,s=d.style,iea=0,sw=(w+a.w),sh=(h+a.h),sws=sw+"px",shs=sh+"px",cn,sww;s.left=(l+a.l)+"px";s.top=(t+a.t)+"px";if(s.width!=sws||s.height!=shs){s.width=sws;s.height=shs;if(!Ext.isIE){cn=d.childNodes;sww=Math.max(0,(sw-12))+"px";cn[0].childNodes[1].style.width=sww;cn[1].childNodes[1].style.width=sww;cn[2].childNodes[1].style.width=sww;cn[1].style.height=Math.max(0,(sh-12))+"px";}}},hide:function(){if(this.el){this.el.dom.style.display="none";Ext.Shadow.Pool.push(this.el);delete this.el;}},setZIndex:function(z){this.zIndex=z;if(this.el){this.el.setStyle("z-index",z);}}};Ext.Shadow.Pool=function(){var p=[],markup=Ext.isIE?'<div class="x-ie-shadow"></div>':'<div class="x-shadow"><div class="xst"><div class="xstl"></div><div class="xstc"></div><div class="xstr"></div></div><div class="xsc"><div class="xsml"></div><div class="xsmc"></div><div class="xsmr"></div></div><div class="xsb"><div class="xsbl"></div><div class="xsbc"></div><div class="xsbr"></div></div></div>';return{pull:function(){var sh=p.shift();if(!sh){sh=Ext.get(Ext.DomHelper.insertHtml("beforeBegin",document.body.firstChild,markup));sh.autoBoxAdjust=false;} +return sh;},push:function(sh){p.push(sh);}};}();Ext.BoxComponent=Ext.extend(Ext.Component,{initComponent:function(){Ext.BoxComponent.superclass.initComponent.call(this);this.addEvents('resize','move');},boxReady:false,deferHeight:false,setSize:function(w,h){if(typeof w=='object'){h=w.height;w=w.width;} +if(Ext.isDefined(w)&&Ext.isDefined(this.boxMinWidth)&&(w<this.boxMinWidth)){w=this.boxMinWidth;} +if(Ext.isDefined(h)&&Ext.isDefined(this.boxMinHeight)&&(h<this.boxMinHeight)){h=this.boxMinHeight;} +if(Ext.isDefined(w)&&Ext.isDefined(this.boxMaxWidth)&&(w>this.boxMaxWidth)){w=this.boxMaxWidth;} +if(Ext.isDefined(h)&&Ext.isDefined(this.boxMaxHeight)&&(h>this.boxMaxHeight)){h=this.boxMaxHeight;} +if(!this.boxReady){this.width=w;this.height=h;return this;} +if(this.cacheSizes!==false&&this.lastSize&&this.lastSize.width==w&&this.lastSize.height==h){return this;} +this.lastSize={width:w,height:h};var adj=this.adjustSize(w,h),aw=adj.width,ah=adj.height,rz;if(aw!==undefined||ah!==undefined){rz=this.getResizeEl();if(!this.deferHeight&&aw!==undefined&&ah!==undefined){rz.setSize(aw,ah);}else if(!this.deferHeight&&ah!==undefined){rz.setHeight(ah);}else if(aw!==undefined){rz.setWidth(aw);} +this.onResize(aw,ah,w,h);this.fireEvent('resize',this,aw,ah,w,h);} +return this;},setWidth:function(width){return this.setSize(width);},setHeight:function(height){return this.setSize(undefined,height);},getSize:function(){return this.getResizeEl().getSize();},getWidth:function(){return this.getResizeEl().getWidth();},getHeight:function(){return this.getResizeEl().getHeight();},getOuterSize:function(){var el=this.getResizeEl();return{width:el.getWidth()+el.getMargins('lr'),height:el.getHeight()+el.getMargins('tb')};},getPosition:function(local){var el=this.getPositionEl();if(local===true){return[el.getLeft(true),el.getTop(true)];} +return this.xy||el.getXY();},getBox:function(local){var pos=this.getPosition(local);var s=this.getSize();s.x=pos[0];s.y=pos[1];return s;},updateBox:function(box){this.setSize(box.width,box.height);this.setPagePosition(box.x,box.y);return this;},getResizeEl:function(){return this.resizeEl||this.el;},setAutoScroll:function(scroll){if(this.rendered){this.getContentTarget().setOverflow(scroll?'auto':'');} +this.autoScroll=scroll;return this;},setPosition:function(x,y){if(x&&typeof x[1]=='number'){y=x[1];x=x[0];} +this.x=x;this.y=y;if(!this.boxReady){return this;} +var adj=this.adjustPosition(x,y);var ax=adj.x,ay=adj.y;var el=this.getPositionEl();if(ax!==undefined||ay!==undefined){if(ax!==undefined&&ay!==undefined){el.setLeftTop(ax,ay);}else if(ax!==undefined){el.setLeft(ax);}else if(ay!==undefined){el.setTop(ay);} +this.onPosition(ax,ay);this.fireEvent('move',this,ax,ay);} +return this;},setPagePosition:function(x,y){if(x&&typeof x[1]=='number'){y=x[1];x=x[0];} +this.pageX=x;this.pageY=y;if(!this.boxReady){return;} +if(x===undefined||y===undefined){return;} +var p=this.getPositionEl().translatePoints(x,y);this.setPosition(p.left,p.top);return this;},afterRender:function(){Ext.BoxComponent.superclass.afterRender.call(this);if(this.resizeEl){this.resizeEl=Ext.get(this.resizeEl);} +if(this.positionEl){this.positionEl=Ext.get(this.positionEl);} +this.boxReady=true;Ext.isDefined(this.autoScroll)&&this.setAutoScroll(this.autoScroll);this.setSize(this.width,this.height);if(this.x||this.y){this.setPosition(this.x,this.y);}else if(this.pageX||this.pageY){this.setPagePosition(this.pageX,this.pageY);}},syncSize:function(){delete this.lastSize;this.setSize(this.autoWidth?undefined:this.getResizeEl().getWidth(),this.autoHeight?undefined:this.getResizeEl().getHeight());return this;},onResize:function(adjWidth,adjHeight,rawWidth,rawHeight){},onPosition:function(x,y){},adjustSize:function(w,h){if(this.autoWidth){w='auto';} +if(this.autoHeight){h='auto';} +return{width:w,height:h};},adjustPosition:function(x,y){return{x:x,y:y};}});Ext.reg('box',Ext.BoxComponent);Ext.Spacer=Ext.extend(Ext.BoxComponent,{autoEl:'div'});Ext.reg('spacer',Ext.Spacer);Ext.SplitBar=function(dragElement,resizingElement,orientation,placement,existingProxy){this.el=Ext.get(dragElement,true);this.el.dom.unselectable="on";this.resizingEl=Ext.get(resizingElement,true);this.orientation=orientation||Ext.SplitBar.HORIZONTAL;this.minSize=0;this.maxSize=2000;this.animate=false;this.useShim=false;this.shim=null;if(!existingProxy){this.proxy=Ext.SplitBar.createProxy(this.orientation);}else{this.proxy=Ext.get(existingProxy).dom;} +this.dd=new Ext.dd.DDProxy(this.el.dom.id,"XSplitBars",{dragElId:this.proxy.id});this.dd.b4StartDrag=this.onStartProxyDrag.createDelegate(this);this.dd.endDrag=this.onEndProxyDrag.createDelegate(this);this.dragSpecs={};this.adapter=new Ext.SplitBar.BasicLayoutAdapter();this.adapter.init(this);if(this.orientation==Ext.SplitBar.HORIZONTAL){this.placement=placement||(this.el.getX()>this.resizingEl.getX()?Ext.SplitBar.LEFT:Ext.SplitBar.RIGHT);this.el.addClass("x-splitbar-h");}else{this.placement=placement||(this.el.getY()>this.resizingEl.getY()?Ext.SplitBar.TOP:Ext.SplitBar.BOTTOM);this.el.addClass("x-splitbar-v");} +this.addEvents("resize","moved","beforeresize","beforeapply");Ext.SplitBar.superclass.constructor.call(this);};Ext.extend(Ext.SplitBar,Ext.util.Observable,{onStartProxyDrag:function(x,y){this.fireEvent("beforeresize",this);this.overlay=Ext.DomHelper.append(document.body,{cls:"x-drag-overlay",html:" "},true);this.overlay.unselectable();this.overlay.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true));this.overlay.show();Ext.get(this.proxy).setDisplayed("block");var size=this.adapter.getElementSize(this);this.activeMinSize=this.getMinimumSize();this.activeMaxSize=this.getMaximumSize();var c1=size-this.activeMinSize;var c2=Math.max(this.activeMaxSize-size,0);if(this.orientation==Ext.SplitBar.HORIZONTAL){this.dd.resetConstraints();this.dd.setXConstraint(this.placement==Ext.SplitBar.LEFT?c1:c2,this.placement==Ext.SplitBar.LEFT?c2:c1,this.tickSize);this.dd.setYConstraint(0,0);}else{this.dd.resetConstraints();this.dd.setXConstraint(0,0);this.dd.setYConstraint(this.placement==Ext.SplitBar.TOP?c1:c2,this.placement==Ext.SplitBar.TOP?c2:c1,this.tickSize);} +this.dragSpecs.startSize=size;this.dragSpecs.startPoint=[x,y];Ext.dd.DDProxy.prototype.b4StartDrag.call(this.dd,x,y);},onEndProxyDrag:function(e){Ext.get(this.proxy).setDisplayed(false);var endPoint=Ext.lib.Event.getXY(e);if(this.overlay){Ext.destroy(this.overlay);delete this.overlay;} +var newSize;if(this.orientation==Ext.SplitBar.HORIZONTAL){newSize=this.dragSpecs.startSize+ +(this.placement==Ext.SplitBar.LEFT?endPoint[0]-this.dragSpecs.startPoint[0]:this.dragSpecs.startPoint[0]-endPoint[0]);}else{newSize=this.dragSpecs.startSize+ +(this.placement==Ext.SplitBar.TOP?endPoint[1]-this.dragSpecs.startPoint[1]:this.dragSpecs.startPoint[1]-endPoint[1]);} +newSize=Math.min(Math.max(newSize,this.activeMinSize),this.activeMaxSize);if(newSize!=this.dragSpecs.startSize){if(this.fireEvent('beforeapply',this,newSize)!==false){this.adapter.setElementSize(this,newSize);this.fireEvent("moved",this,newSize);this.fireEvent("resize",this,newSize);}}},getAdapter:function(){return this.adapter;},setAdapter:function(adapter){this.adapter=adapter;this.adapter.init(this);},getMinimumSize:function(){return this.minSize;},setMinimumSize:function(minSize){this.minSize=minSize;},getMaximumSize:function(){return this.maxSize;},setMaximumSize:function(maxSize){this.maxSize=maxSize;},setCurrentSize:function(size){var oldAnimate=this.animate;this.animate=false;this.adapter.setElementSize(this,size);this.animate=oldAnimate;},destroy:function(removeEl){Ext.destroy(this.shim,Ext.get(this.proxy));this.dd.unreg();if(removeEl){this.el.remove();} +this.purgeListeners();}});Ext.SplitBar.createProxy=function(dir){var proxy=new Ext.Element(document.createElement("div"));document.body.appendChild(proxy.dom);proxy.unselectable();var cls='x-splitbar-proxy';proxy.addClass(cls+' '+(dir==Ext.SplitBar.HORIZONTAL?cls+'-h':cls+'-v'));return proxy.dom;};Ext.SplitBar.BasicLayoutAdapter=function(){};Ext.SplitBar.BasicLayoutAdapter.prototype={init:function(s){},getElementSize:function(s){if(s.orientation==Ext.SplitBar.HORIZONTAL){return s.resizingEl.getWidth();}else{return s.resizingEl.getHeight();}},setElementSize:function(s,newSize,onComplete){if(s.orientation==Ext.SplitBar.HORIZONTAL){if(!s.animate){s.resizingEl.setWidth(newSize);if(onComplete){onComplete(s,newSize);}}else{s.resizingEl.setWidth(newSize,true,.1,onComplete,'easeOut');}}else{if(!s.animate){s.resizingEl.setHeight(newSize);if(onComplete){onComplete(s,newSize);}}else{s.resizingEl.setHeight(newSize,true,.1,onComplete,'easeOut');}}}};Ext.SplitBar.AbsoluteLayoutAdapter=function(container){this.basic=new Ext.SplitBar.BasicLayoutAdapter();this.container=Ext.get(container);};Ext.SplitBar.AbsoluteLayoutAdapter.prototype={init:function(s){this.basic.init(s);},getElementSize:function(s){return this.basic.getElementSize(s);},setElementSize:function(s,newSize,onComplete){this.basic.setElementSize(s,newSize,this.moveSplitter.createDelegate(this,[s]));},moveSplitter:function(s){var yes=Ext.SplitBar;switch(s.placement){case yes.LEFT:s.el.setX(s.resizingEl.getRight());break;case yes.RIGHT:s.el.setStyle("right",(this.container.getWidth()-s.resizingEl.getLeft())+"px");break;case yes.TOP:s.el.setY(s.resizingEl.getBottom());break;case yes.BOTTOM:s.el.setY(s.resizingEl.getTop()-s.el.getHeight());break;}}};Ext.SplitBar.VERTICAL=1;Ext.SplitBar.HORIZONTAL=2;Ext.SplitBar.LEFT=1;Ext.SplitBar.RIGHT=2;Ext.SplitBar.TOP=3;Ext.SplitBar.BOTTOM=4;Ext.Container=Ext.extend(Ext.BoxComponent,{bufferResize:50,autoDestroy:true,forceLayout:false,defaultType:'panel',resizeEvent:'resize',bubbleEvents:['add','remove'],initComponent:function(){Ext.Container.superclass.initComponent.call(this);this.addEvents('afterlayout','beforeadd','beforeremove','add','remove');var items=this.items;if(items){delete this.items;this.add(items);}},initItems:function(){if(!this.items){this.items=new Ext.util.MixedCollection(false,this.getComponentId);this.getLayout();}},setLayout:function(layout){if(this.layout&&this.layout!=layout){this.layout.setContainer(null);} +this.layout=layout;this.initItems();layout.setContainer(this);},afterRender:function(){Ext.Container.superclass.afterRender.call(this);if(!this.layout){this.layout='auto';} +if(Ext.isObject(this.layout)&&!this.layout.layout){this.layoutConfig=this.layout;this.layout=this.layoutConfig.type;} +if(Ext.isString(this.layout)){this.layout=new Ext.Container.LAYOUTS[this.layout.toLowerCase()](this.layoutConfig);} +this.setLayout(this.layout);if(this.activeItem!==undefined&&this.layout.setActiveItem){var item=this.activeItem;delete this.activeItem;this.layout.setActiveItem(item);} +if(!this.ownerCt){this.doLayout(false,true);} +if(this.monitorResize===true){Ext.EventManager.onWindowResize(this.doLayout,this,[false]);}},getLayoutTarget:function(){return this.el;},getComponentId:function(comp){return comp.getItemId();},add:function(comp){this.initItems();var args=arguments.length>1;if(args||Ext.isArray(comp)){var result=[];Ext.each(args?arguments:comp,function(c){result.push(this.add(c));},this);return result;} +var c=this.lookupComponent(this.applyDefaults(comp));var index=this.items.length;if(this.fireEvent('beforeadd',this,c,index)!==false&&this.onBeforeAdd(c)!==false){this.items.add(c);c.onAdded(this,index);this.onAdd(c);this.fireEvent('add',this,c,index);} +return c;},onAdd:function(c){},onAdded:function(container,pos){this.ownerCt=container;this.initRef();this.cascade(function(c){c.initRef();});this.fireEvent('added',this,container,pos);},insert:function(index,comp){var args=arguments,length=args.length,result=[],i,c;this.initItems();if(length>2){for(i=length-1;i>=1;--i){result.push(this.insert(index,args[i]));} +return result;} +c=this.lookupComponent(this.applyDefaults(comp));index=Math.min(index,this.items.length);if(this.fireEvent('beforeadd',this,c,index)!==false&&this.onBeforeAdd(c)!==false){if(c.ownerCt==this){this.items.remove(c);} +this.items.insert(index,c);c.onAdded(this,index);this.onAdd(c);this.fireEvent('add',this,c,index);} +return c;},applyDefaults:function(c){var d=this.defaults;if(d){if(Ext.isFunction(d)){d=d.call(this,c);} +if(Ext.isString(c)){c=Ext.ComponentMgr.get(c);Ext.apply(c,d);}else if(!c.events){Ext.applyIf(c.isAction?c.initialConfig:c,d);}else{Ext.apply(c,d);}} +return c;},onBeforeAdd:function(item){if(item.ownerCt){item.ownerCt.remove(item,false);} +if(this.hideBorders===true){item.border=(item.border===true);}},remove:function(comp,autoDestroy){this.initItems();var c=this.getComponent(comp);if(c&&this.fireEvent('beforeremove',this,c)!==false){this.doRemove(c,autoDestroy);this.fireEvent('remove',this,c);} +return c;},onRemove:function(c){},doRemove:function(c,autoDestroy){var l=this.layout,hasLayout=l&&this.rendered;if(hasLayout){l.onRemove(c);} +this.items.remove(c);c.onRemoved();this.onRemove(c);if(autoDestroy===true||(autoDestroy!==false&&this.autoDestroy)){c.destroy();} +if(hasLayout){l.afterRemove(c);}},removeAll:function(autoDestroy){this.initItems();var item,rem=[],items=[];this.items.each(function(i){rem.push(i);});for(var i=0,len=rem.length;i<len;++i){item=rem[i];this.remove(item,autoDestroy);if(item.ownerCt!==this){items.push(item);}} +return items;},getComponent:function(comp){if(Ext.isObject(comp)){comp=comp.getItemId();} +return this.items.get(comp);},lookupComponent:function(comp){if(Ext.isString(comp)){return Ext.ComponentMgr.get(comp);}else if(!comp.events){return this.createComponent(comp);} +return comp;},createComponent:function(config,defaultType){if(config.render){return config;} +var c=Ext.create(Ext.apply({ownerCt:this},config),defaultType||this.defaultType);delete c.initialConfig.ownerCt;delete c.ownerCt;return c;},canLayout:function(){var el=this.getVisibilityEl();return el&&el.dom&&!el.isStyle("display","none");},doLayout:function(shallow,force){var rendered=this.rendered,forceLayout=force||this.forceLayout;if(this.collapsed||!this.canLayout()){this.deferLayout=this.deferLayout||!shallow;if(!forceLayout){return;} +shallow=shallow&&!this.deferLayout;}else{delete this.deferLayout;} +if(rendered&&this.layout){this.layout.layout();} +if(shallow!==true&&this.items){var cs=this.items.items;for(var i=0,len=cs.length;i<len;i++){var c=cs[i];if(c.doLayout){c.doLayout(false,forceLayout);}}} +if(rendered){this.onLayout(shallow,forceLayout);} +this.hasLayout=true;delete this.forceLayout;},onLayout:Ext.emptyFn,shouldBufferLayout:function(){var hl=this.hasLayout;if(this.ownerCt){return hl?!this.hasLayoutPending():false;} +return hl;},hasLayoutPending:function(){var pending=false;this.ownerCt.bubble(function(c){if(c.layoutPending){pending=true;return false;}});return pending;},onShow:function(){Ext.Container.superclass.onShow.call(this);if(Ext.isDefined(this.deferLayout)){delete this.deferLayout;this.doLayout(true);}},getLayout:function(){if(!this.layout){var layout=new Ext.layout.AutoLayout(this.layoutConfig);this.setLayout(layout);} +return this.layout;},beforeDestroy:function(){var c;if(this.items){while(c=this.items.first()){this.doRemove(c,true);}} +if(this.monitorResize){Ext.EventManager.removeResizeListener(this.doLayout,this);} +Ext.destroy(this.layout);Ext.Container.superclass.beforeDestroy.call(this);},cascade:function(fn,scope,args){if(fn.apply(scope||this,args||[this])!==false){if(this.items){var cs=this.items.items;for(var i=0,len=cs.length;i<len;i++){if(cs[i].cascade){cs[i].cascade(fn,scope,args);}else{fn.apply(scope||cs[i],args||[cs[i]]);}}}} +return this;},findById:function(id){var m=null,ct=this;this.cascade(function(c){if(ct!=c&&c.id===id){m=c;return false;}});return m;},findByType:function(xtype,shallow){return this.findBy(function(c){return c.isXType(xtype,shallow);});},find:function(prop,value){return this.findBy(function(c){return c[prop]===value;});},findBy:function(fn,scope){var m=[],ct=this;this.cascade(function(c){if(ct!=c&&fn.call(scope||c,c,ct)===true){m.push(c);}});return m;},get:function(key){return this.getComponent(key);}});Ext.Container.LAYOUTS={};Ext.reg('container',Ext.Container);Ext.layout.ContainerLayout=Ext.extend(Object,{monitorResize:false,activeItem:null,constructor:function(config){this.id=Ext.id(null,'ext-layout-');Ext.apply(this,config);},type:'container',IEMeasureHack:function(target,viewFlag){var tChildren=target.dom.childNodes,tLen=tChildren.length,c,d=[],e,i,ret;for(i=0;i<tLen;i++){c=tChildren[i];e=Ext.get(c);if(e){d[i]=e.getStyle('display');e.setStyle({display:'none'});}} +ret=target?target.getViewSize(viewFlag):{};for(i=0;i<tLen;i++){c=tChildren[i];e=Ext.get(c);if(e){e.setStyle({display:d[i]});}} +return ret;},getLayoutTargetSize:Ext.EmptyFn,layout:function(){var ct=this.container,target=ct.getLayoutTarget();if(!(this.hasLayout||Ext.isEmpty(this.targetCls))){target.addClass(this.targetCls);} +this.onLayout(ct,target);ct.fireEvent('afterlayout',ct,this);},onLayout:function(ct,target){this.renderAll(ct,target);},isValidParent:function(c,target){return target&&c.getPositionEl().dom.parentNode==(target.dom||target);},renderAll:function(ct,target){var items=ct.items.items,i,c,len=items.length;for(i=0;i<len;i++){c=items[i];if(c&&(!c.rendered||!this.isValidParent(c,target))){this.renderItem(c,i,target);}}},renderItem:function(c,position,target){if(c){if(!c.rendered){c.render(target,position);this.configureItem(c);}else if(!this.isValidParent(c,target)){if(Ext.isNumber(position)){position=target.dom.childNodes[position];} +target.dom.insertBefore(c.getPositionEl().dom,position||null);c.container=target;this.configureItem(c);}}},getRenderedItems:function(ct){var t=ct.getLayoutTarget(),cti=ct.items.items,len=cti.length,i,c,items=[];for(i=0;i<len;i++){if((c=cti[i]).rendered&&this.isValidParent(c,t)&&c.shouldLayout!==false){items.push(c);}};return items;},configureItem:function(c){if(this.extraCls){var t=c.getPositionEl?c.getPositionEl():c;t.addClass(this.extraCls);} +if(c.doLayout&&this.forceLayout){c.doLayout();} +if(this.renderHidden&&c!=this.activeItem){c.hide();}},onRemove:function(c){if(this.activeItem==c){delete this.activeItem;} +if(c.rendered&&this.extraCls){var t=c.getPositionEl?c.getPositionEl():c;t.removeClass(this.extraCls);}},afterRemove:function(c){if(c.removeRestore){c.removeMode='container';delete c.removeRestore;}},onResize:function(){var ct=this.container,b;if(ct.collapsed){return;} +if(b=ct.bufferResize&&ct.shouldBufferLayout()){if(!this.resizeTask){this.resizeTask=new Ext.util.DelayedTask(this.runLayout,this);this.resizeBuffer=Ext.isNumber(b)?b:50;} +ct.layoutPending=true;this.resizeTask.delay(this.resizeBuffer);}else{this.runLayout();}},runLayout:function(){var ct=this.container;this.layout();ct.onLayout();delete ct.layoutPending;},setContainer:function(ct){if(this.monitorResize&&ct!=this.container){var old=this.container;if(old){old.un(old.resizeEvent,this.onResize,this);} +if(ct){ct.on(ct.resizeEvent,this.onResize,this);}} +this.container=ct;},parseMargins:function(v){if(Ext.isNumber(v)){v=v.toString();} +var ms=v.split(' '),len=ms.length;if(len==1){ms[1]=ms[2]=ms[3]=ms[0];}else if(len==2){ms[2]=ms[0];ms[3]=ms[1];}else if(len==3){ms[3]=ms[1];} +return{top:parseInt(ms[0],10)||0,right:parseInt(ms[1],10)||0,bottom:parseInt(ms[2],10)||0,left:parseInt(ms[3],10)||0};},fieldTpl:(function(){var t=new Ext.Template('<div class="x-form-item {itemCls}" tabIndex="-1">','<label for="{id}" style="{labelStyle}" class="x-form-item-label">{label}{labelSeparator}</label>','<div class="x-form-element" id="x-form-el-{id}" style="{elementStyle}">','</div><div class="{clearCls}"></div>','</div>');t.disableFormats=true;return t.compile();})(),destroy:function(){if(this.resizeTask&&this.resizeTask.cancel){this.resizeTask.cancel();} +if(this.container){this.container.un(this.container.resizeEvent,this.onResize,this);} +if(!Ext.isEmpty(this.targetCls)){var target=this.container.getLayoutTarget();if(target){target.removeClass(this.targetCls);}}}});Ext.layout.AutoLayout=Ext.extend(Ext.layout.ContainerLayout,{type:'auto',monitorResize:true,onLayout:function(ct,target){Ext.layout.AutoLayout.superclass.onLayout.call(this,ct,target);var cs=this.getRenderedItems(ct),len=cs.length,i,c;for(i=0;i<len;i++){c=cs[i];if(c.doLayout){c.doLayout(true);}}}});Ext.Container.LAYOUTS['auto']=Ext.layout.AutoLayout;Ext.layout.FitLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:'fit',getLayoutTargetSize:function(){var target=this.container.getLayoutTarget();if(!target){return{};} +return target.getStyleSize();},onLayout:function(ct,target){Ext.layout.FitLayout.superclass.onLayout.call(this,ct,target);if(!ct.collapsed){this.setItemSize(this.activeItem||ct.items.itemAt(0),this.getLayoutTargetSize());}},setItemSize:function(item,size){if(item&&size.height>0){item.setSize(size);}}});Ext.Container.LAYOUTS['fit']=Ext.layout.FitLayout;Ext.layout.CardLayout=Ext.extend(Ext.layout.FitLayout,{deferredRender:false,layoutOnCardChange:false,renderHidden:true,type:'card',setActiveItem:function(item){var ai=this.activeItem,ct=this.container;item=ct.getComponent(item);if(item&&ai!=item){if(ai){ai.hide();if(ai.hidden!==true){return false;} +ai.fireEvent('deactivate',ai);} +var layout=item.doLayout&&(this.layoutOnCardChange||!item.rendered);this.activeItem=item;delete item.deferLayout;item.show();this.layout();if(layout){item.doLayout();} +item.fireEvent('activate',item);}},renderAll:function(ct,target){if(this.deferredRender){this.renderItem(this.activeItem,undefined,target);}else{Ext.layout.CardLayout.superclass.renderAll.call(this,ct,target);}}});Ext.Container.LAYOUTS['card']=Ext.layout.CardLayout;Ext.layout.AnchorLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:'anchor',defaultAnchor:'100%',parseAnchorRE:/^(r|right|b|bottom)$/i,getLayoutTargetSize:function(){var target=this.container.getLayoutTarget(),ret={};if(target){ret=target.getViewSize();if(Ext.isIE&&Ext.isStrict&&ret.width==0){ret=target.getStyleSize();} +ret.width-=target.getPadding('lr');ret.height-=target.getPadding('tb');} +return ret;},onLayout:function(container,target){Ext.layout.AnchorLayout.superclass.onLayout.call(this,container,target);var size=this.getLayoutTargetSize(),containerWidth=size.width,containerHeight=size.height,overflow=target.getStyle('overflow'),components=this.getRenderedItems(container),len=components.length,boxes=[],box,anchorWidth,anchorHeight,component,anchorSpec,calcWidth,calcHeight,anchorsArray,totalHeight=0,i,el;if(containerWidth<20&&containerHeight<20){return;} +if(container.anchorSize){if(typeof container.anchorSize=='number'){anchorWidth=container.anchorSize;}else{anchorWidth=container.anchorSize.width;anchorHeight=container.anchorSize.height;}}else{anchorWidth=container.initialConfig.width;anchorHeight=container.initialConfig.height;} +for(i=0;i<len;i++){component=components[i];el=component.getPositionEl();if(!component.anchor&&component.items&&!Ext.isNumber(component.width)&&!(Ext.isIE6&&Ext.isStrict)){component.anchor=this.defaultAnchor;} +if(component.anchor){anchorSpec=component.anchorSpec;if(!anchorSpec){anchorsArray=component.anchor.split(' ');component.anchorSpec=anchorSpec={right:this.parseAnchor(anchorsArray[0],component.initialConfig.width,anchorWidth),bottom:this.parseAnchor(anchorsArray[1],component.initialConfig.height,anchorHeight)};} +calcWidth=anchorSpec.right?this.adjustWidthAnchor(anchorSpec.right(containerWidth)-el.getMargins('lr'),component):undefined;calcHeight=anchorSpec.bottom?this.adjustHeightAnchor(anchorSpec.bottom(containerHeight)-el.getMargins('tb'),component):undefined;if(calcWidth||calcHeight){boxes.push({component:component,width:calcWidth||undefined,height:calcHeight||undefined});}}} +for(i=0,len=boxes.length;i<len;i++){box=boxes[i];box.component.setSize(box.width,box.height);} +if(overflow&&overflow!='hidden'&&!this.adjustmentPass){var newTargetSize=this.getLayoutTargetSize();if(newTargetSize.width!=size.width||newTargetSize.height!=size.height){this.adjustmentPass=true;this.onLayout(container,target);}} +delete this.adjustmentPass;},parseAnchor:function(a,start,cstart){if(a&&a!='none'){var last;if(this.parseAnchorRE.test(a)){var diff=cstart-start;return function(v){if(v!==last){last=v;return v-diff;}};}else if(a.indexOf('%')!=-1){var ratio=parseFloat(a.replace('%',''))*.01;return function(v){if(v!==last){last=v;return Math.floor(v*ratio);}};}else{a=parseInt(a,10);if(!isNaN(a)){return function(v){if(v!==last){last=v;return v+a;}};}}} +return false;},adjustWidthAnchor:function(value,comp){return value;},adjustHeightAnchor:function(value,comp){return value;}});Ext.Container.LAYOUTS['anchor']=Ext.layout.AnchorLayout;Ext.layout.ColumnLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:'column',extraCls:'x-column',scrollOffset:0,targetCls:'x-column-layout-ct',isValidParent:function(c,target){return this.innerCt&&c.getPositionEl().dom.parentNode==this.innerCt.dom;},getLayoutTargetSize:function(){var target=this.container.getLayoutTarget(),ret;if(target){ret=target.getViewSize();if(Ext.isIE&&Ext.isStrict&&ret.width==0){ret=target.getStyleSize();} +ret.width-=target.getPadding('lr');ret.height-=target.getPadding('tb');} +return ret;},renderAll:function(ct,target){if(!this.innerCt){this.innerCt=target.createChild({cls:'x-column-inner'});this.innerCt.createChild({cls:'x-clear'});} +Ext.layout.ColumnLayout.superclass.renderAll.call(this,ct,this.innerCt);},onLayout:function(ct,target){var cs=ct.items.items,len=cs.length,c,i,m,margins=[];this.renderAll(ct,target);var size=this.getLayoutTargetSize();if(size.width<1&&size.height<1){return;} +var w=size.width-this.scrollOffset,h=size.height,pw=w;this.innerCt.setWidth(w);for(i=0;i<len;i++){c=cs[i];m=c.getPositionEl().getMargins('lr');margins[i]=m;if(!c.columnWidth){pw-=(c.getWidth()+m);}} +pw=pw<0?0:pw;for(i=0;i<len;i++){c=cs[i];m=margins[i];if(c.columnWidth){c.setSize(Math.floor(c.columnWidth*pw)-m);}} +if(Ext.isIE){if(i=target.getStyle('overflow')&&i!='hidden'&&!this.adjustmentPass){var ts=this.getLayoutTargetSize();if(ts.width!=size.width){this.adjustmentPass=true;this.onLayout(ct,target);}}} +delete this.adjustmentPass;}});Ext.Container.LAYOUTS['column']=Ext.layout.ColumnLayout;Ext.layout.BorderLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,rendered:false,type:'border',targetCls:'x-border-layout-ct',getLayoutTargetSize:function(){var target=this.container.getLayoutTarget();return target?target.getViewSize():{};},onLayout:function(ct,target){var collapsed,i,c,pos,items=ct.items.items,len=items.length;if(!this.rendered){collapsed=[];for(i=0;i<len;i++){c=items[i];pos=c.region;if(c.collapsed){collapsed.push(c);} +c.collapsed=false;if(!c.rendered){c.render(target,i);c.getPositionEl().addClass('x-border-panel');} +this[pos]=pos!='center'&&c.split?new Ext.layout.BorderLayout.SplitRegion(this,c.initialConfig,pos):new Ext.layout.BorderLayout.Region(this,c.initialConfig,pos);this[pos].render(target,c);} +this.rendered=true;} +var size=this.getLayoutTargetSize();if(size.width<20||size.height<20){if(collapsed){this.restoreCollapsed=collapsed;} +return;}else if(this.restoreCollapsed){collapsed=this.restoreCollapsed;delete this.restoreCollapsed;} +var w=size.width,h=size.height,centerW=w,centerH=h,centerY=0,centerX=0,n=this.north,s=this.south,west=this.west,e=this.east,c=this.center,b,m,totalWidth,totalHeight;if(!c&&Ext.layout.BorderLayout.WARN!==false){throw'No center region defined in BorderLayout '+ct.id;} +if(n&&n.isVisible()){b=n.getSize();m=n.getMargins();b.width=w-(m.left+m.right);b.x=m.left;b.y=m.top;centerY=b.height+b.y+m.bottom;centerH-=centerY;n.applyLayout(b);} +if(s&&s.isVisible()){b=s.getSize();m=s.getMargins();b.width=w-(m.left+m.right);b.x=m.left;totalHeight=(b.height+m.top+m.bottom);b.y=h-totalHeight+m.top;centerH-=totalHeight;s.applyLayout(b);} +if(west&&west.isVisible()){b=west.getSize();m=west.getMargins();b.height=centerH-(m.top+m.bottom);b.x=m.left;b.y=centerY+m.top;totalWidth=(b.width+m.left+m.right);centerX+=totalWidth;centerW-=totalWidth;west.applyLayout(b);} +if(e&&e.isVisible()){b=e.getSize();m=e.getMargins();b.height=centerH-(m.top+m.bottom);totalWidth=(b.width+m.left+m.right);b.x=w-totalWidth+m.left;b.y=centerY+m.top;centerW-=totalWidth;e.applyLayout(b);} +if(c){m=c.getMargins();var centerBox={x:centerX+m.left,y:centerY+m.top,width:centerW-(m.left+m.right),height:centerH-(m.top+m.bottom)};c.applyLayout(centerBox);} +if(collapsed){for(i=0,len=collapsed.length;i<len;i++){collapsed[i].collapse(false);}} +if(Ext.isIE&&Ext.isStrict){target.repaint();} +if(i=target.getStyle('overflow')&&i!='hidden'&&!this.adjustmentPass){var ts=this.getLayoutTargetSize();if(ts.width!=size.width||ts.height!=size.height){this.adjustmentPass=true;this.onLayout(ct,target);}} +delete this.adjustmentPass;},destroy:function(){var r=['north','south','east','west'],i,region;for(i=0;i<r.length;i++){region=this[r[i]];if(region){if(region.destroy){region.destroy();}else if(region.split){region.split.destroy(true);}}} +Ext.layout.BorderLayout.superclass.destroy.call(this);}});Ext.layout.BorderLayout.Region=function(layout,config,pos){Ext.apply(this,config);this.layout=layout;this.position=pos;this.state={};if(typeof this.margins=='string'){this.margins=this.layout.parseMargins(this.margins);} +this.margins=Ext.applyIf(this.margins||{},this.defaultMargins);if(this.collapsible){if(typeof this.cmargins=='string'){this.cmargins=this.layout.parseMargins(this.cmargins);} +if(this.collapseMode=='mini'&&!this.cmargins){this.cmargins={left:0,top:0,right:0,bottom:0};}else{this.cmargins=Ext.applyIf(this.cmargins||{},pos=='north'||pos=='south'?this.defaultNSCMargins:this.defaultEWCMargins);}}};Ext.layout.BorderLayout.Region.prototype={collapsible:false,split:false,floatable:true,minWidth:50,minHeight:50,defaultMargins:{left:0,top:0,right:0,bottom:0},defaultNSCMargins:{left:5,top:5,right:5,bottom:5},defaultEWCMargins:{left:5,top:0,right:5,bottom:0},floatingZIndex:100,isCollapsed:false,render:function(ct,p){this.panel=p;p.el.enableDisplayMode();this.targetEl=ct;this.el=p.el;var gs=p.getState,ps=this.position;p.getState=function(){return Ext.apply(gs.call(p)||{},this.state);}.createDelegate(this);if(ps!='center'){p.allowQueuedExpand=false;p.on({beforecollapse:this.beforeCollapse,collapse:this.onCollapse,beforeexpand:this.beforeExpand,expand:this.onExpand,hide:this.onHide,show:this.onShow,scope:this});if(this.collapsible||this.floatable){p.collapseEl='el';p.slideAnchor=this.getSlideAnchor();} +if(p.tools&&p.tools.toggle){p.tools.toggle.addClass('x-tool-collapse-'+ps);p.tools.toggle.addClassOnOver('x-tool-collapse-'+ps+'-over');}}},getCollapsedEl:function(){if(!this.collapsedEl){if(!this.toolTemplate){var tt=new Ext.Template('<div class="x-tool x-tool-{id}"> </div>');tt.disableFormats=true;tt.compile();Ext.layout.BorderLayout.Region.prototype.toolTemplate=tt;} +this.collapsedEl=this.targetEl.createChild({cls:"x-layout-collapsed x-layout-collapsed-"+this.position,id:this.panel.id+'-xcollapsed'});this.collapsedEl.enableDisplayMode('block');if(this.collapseMode=='mini'){this.collapsedEl.addClass('x-layout-cmini-'+this.position);this.miniCollapsedEl=this.collapsedEl.createChild({cls:"x-layout-mini x-layout-mini-"+this.position,html:" "});this.miniCollapsedEl.addClassOnOver('x-layout-mini-over');this.collapsedEl.addClassOnOver("x-layout-collapsed-over");this.collapsedEl.on('click',this.onExpandClick,this,{stopEvent:true});}else{if(this.collapsible!==false&&!this.hideCollapseTool){var t=this.expandToolEl=this.toolTemplate.append(this.collapsedEl.dom,{id:'expand-'+this.position},true);t.addClassOnOver('x-tool-expand-'+this.position+'-over');t.on('click',this.onExpandClick,this,{stopEvent:true});} +if(this.floatable!==false||this.titleCollapse){this.collapsedEl.addClassOnOver("x-layout-collapsed-over");this.collapsedEl.on("click",this[this.floatable?'collapseClick':'onExpandClick'],this);}}} +return this.collapsedEl;},onExpandClick:function(e){if(this.isSlid){this.panel.expand(false);}else{this.panel.expand();}},onCollapseClick:function(e){this.panel.collapse();},beforeCollapse:function(p,animate){this.lastAnim=animate;if(this.splitEl){this.splitEl.hide();} +this.getCollapsedEl().show();var el=this.panel.getEl();this.originalZIndex=el.getStyle('z-index');el.setStyle('z-index',100);this.isCollapsed=true;this.layout.layout();},onCollapse:function(animate){this.panel.el.setStyle('z-index',1);if(this.lastAnim===false||this.panel.animCollapse===false){this.getCollapsedEl().dom.style.visibility='visible';}else{this.getCollapsedEl().slideIn(this.panel.slideAnchor,{duration:.2});} +this.state.collapsed=true;this.panel.saveState();},beforeExpand:function(animate){if(this.isSlid){this.afterSlideIn();} +var c=this.getCollapsedEl();this.el.show();if(this.position=='east'||this.position=='west'){this.panel.setSize(undefined,c.getHeight());}else{this.panel.setSize(c.getWidth(),undefined);} +c.hide();c.dom.style.visibility='hidden';this.panel.el.setStyle('z-index',this.floatingZIndex);},onExpand:function(){this.isCollapsed=false;if(this.splitEl){this.splitEl.show();} +this.layout.layout();this.panel.el.setStyle('z-index',this.originalZIndex);this.state.collapsed=false;this.panel.saveState();},collapseClick:function(e){if(this.isSlid){e.stopPropagation();this.slideIn();}else{e.stopPropagation();this.slideOut();}},onHide:function(){if(this.isCollapsed){this.getCollapsedEl().hide();}else if(this.splitEl){this.splitEl.hide();}},onShow:function(){if(this.isCollapsed){this.getCollapsedEl().show();}else if(this.splitEl){this.splitEl.show();}},isVisible:function(){return!this.panel.hidden;},getMargins:function(){return this.isCollapsed&&this.cmargins?this.cmargins:this.margins;},getSize:function(){return this.isCollapsed?this.getCollapsedEl().getSize():this.panel.getSize();},setPanel:function(panel){this.panel=panel;},getMinWidth:function(){return this.minWidth;},getMinHeight:function(){return this.minHeight;},applyLayoutCollapsed:function(box){var ce=this.getCollapsedEl();ce.setLeftTop(box.x,box.y);ce.setSize(box.width,box.height);},applyLayout:function(box){if(this.isCollapsed){this.applyLayoutCollapsed(box);}else{this.panel.setPosition(box.x,box.y);this.panel.setSize(box.width,box.height);}},beforeSlide:function(){this.panel.beforeEffect();},afterSlide:function(){this.panel.afterEffect();},initAutoHide:function(){if(this.autoHide!==false){if(!this.autoHideHd){this.autoHideSlideTask=new Ext.util.DelayedTask(this.slideIn,this);this.autoHideHd={"mouseout":function(e){if(!e.within(this.el,true)){this.autoHideSlideTask.delay(500);}},"mouseover":function(e){this.autoHideSlideTask.cancel();},scope:this};} +this.el.on(this.autoHideHd);this.collapsedEl.on(this.autoHideHd);}},clearAutoHide:function(){if(this.autoHide!==false){this.el.un("mouseout",this.autoHideHd.mouseout);this.el.un("mouseover",this.autoHideHd.mouseover);this.collapsedEl.un("mouseout",this.autoHideHd.mouseout);this.collapsedEl.un("mouseover",this.autoHideHd.mouseover);}},clearMonitor:function(){Ext.getDoc().un("click",this.slideInIf,this);},slideOut:function(){if(this.isSlid||this.el.hasActiveFx()){return;} +this.isSlid=true;var ts=this.panel.tools,dh,pc;if(ts&&ts.toggle){ts.toggle.hide();} +this.el.show();pc=this.panel.collapsed;this.panel.collapsed=false;if(this.position=='east'||this.position=='west'){dh=this.panel.deferHeight;this.panel.deferHeight=false;this.panel.setSize(undefined,this.collapsedEl.getHeight());this.panel.deferHeight=dh;}else{this.panel.setSize(this.collapsedEl.getWidth(),undefined);} +this.panel.collapsed=pc;this.restoreLT=[this.el.dom.style.left,this.el.dom.style.top];this.el.alignTo(this.collapsedEl,this.getCollapseAnchor());this.el.setStyle("z-index",this.floatingZIndex+2);this.panel.el.replaceClass('x-panel-collapsed','x-panel-floating');if(this.animFloat!==false){this.beforeSlide();this.el.slideIn(this.getSlideAnchor(),{callback:function(){this.afterSlide();this.initAutoHide();Ext.getDoc().on("click",this.slideInIf,this);},scope:this,block:true});}else{this.initAutoHide();Ext.getDoc().on("click",this.slideInIf,this);}},afterSlideIn:function(){this.clearAutoHide();this.isSlid=false;this.clearMonitor();this.el.setStyle("z-index","");this.panel.el.replaceClass('x-panel-floating','x-panel-collapsed');this.el.dom.style.left=this.restoreLT[0];this.el.dom.style.top=this.restoreLT[1];var ts=this.panel.tools;if(ts&&ts.toggle){ts.toggle.show();}},slideIn:function(cb){if(!this.isSlid||this.el.hasActiveFx()){Ext.callback(cb);return;} +this.isSlid=false;if(this.animFloat!==false){this.beforeSlide();this.el.slideOut(this.getSlideAnchor(),{callback:function(){this.el.hide();this.afterSlide();this.afterSlideIn();Ext.callback(cb);},scope:this,block:true});}else{this.el.hide();this.afterSlideIn();}},slideInIf:function(e){if(!e.within(this.el)){this.slideIn();}},anchors:{"west":"left","east":"right","north":"top","south":"bottom"},sanchors:{"west":"l","east":"r","north":"t","south":"b"},canchors:{"west":"tl-tr","east":"tr-tl","north":"tl-bl","south":"bl-tl"},getAnchor:function(){return this.anchors[this.position];},getCollapseAnchor:function(){return this.canchors[this.position];},getSlideAnchor:function(){return this.sanchors[this.position];},getAlignAdj:function(){var cm=this.cmargins;switch(this.position){case"west":return[0,0];break;case"east":return[0,0];break;case"north":return[0,0];break;case"south":return[0,0];break;}},getExpandAdj:function(){var c=this.collapsedEl,cm=this.cmargins;switch(this.position){case"west":return[-(cm.right+c.getWidth()+cm.left),0];break;case"east":return[cm.right+c.getWidth()+cm.left,0];break;case"north":return[0,-(cm.top+cm.bottom+c.getHeight())];break;case"south":return[0,cm.top+cm.bottom+c.getHeight()];break;}},destroy:function(){if(this.autoHideSlideTask&&this.autoHideSlideTask.cancel){this.autoHideSlideTask.cancel();} +Ext.destroyMembers(this,'miniCollapsedEl','collapsedEl','expandToolEl');}};Ext.layout.BorderLayout.SplitRegion=function(layout,config,pos){Ext.layout.BorderLayout.SplitRegion.superclass.constructor.call(this,layout,config,pos);this.applyLayout=this.applyFns[pos];};Ext.extend(Ext.layout.BorderLayout.SplitRegion,Ext.layout.BorderLayout.Region,{splitTip:"Drag to resize.",collapsibleSplitTip:"Drag to resize. Double click to hide.",useSplitTips:false,splitSettings:{north:{orientation:Ext.SplitBar.VERTICAL,placement:Ext.SplitBar.TOP,maxFn:'getVMaxSize',minProp:'minHeight',maxProp:'maxHeight'},south:{orientation:Ext.SplitBar.VERTICAL,placement:Ext.SplitBar.BOTTOM,maxFn:'getVMaxSize',minProp:'minHeight',maxProp:'maxHeight'},east:{orientation:Ext.SplitBar.HORIZONTAL,placement:Ext.SplitBar.RIGHT,maxFn:'getHMaxSize',minProp:'minWidth',maxProp:'maxWidth'},west:{orientation:Ext.SplitBar.HORIZONTAL,placement:Ext.SplitBar.LEFT,maxFn:'getHMaxSize',minProp:'minWidth',maxProp:'maxWidth'}},applyFns:{west:function(box){if(this.isCollapsed){return this.applyLayoutCollapsed(box);} +var sd=this.splitEl.dom,s=sd.style;this.panel.setPosition(box.x,box.y);var sw=sd.offsetWidth;s.left=(box.x+box.width-sw)+'px';s.top=(box.y)+'px';s.height=Math.max(0,box.height)+'px';this.panel.setSize(box.width-sw,box.height);},east:function(box){if(this.isCollapsed){return this.applyLayoutCollapsed(box);} +var sd=this.splitEl.dom,s=sd.style;var sw=sd.offsetWidth;this.panel.setPosition(box.x+sw,box.y);s.left=(box.x)+'px';s.top=(box.y)+'px';s.height=Math.max(0,box.height)+'px';this.panel.setSize(box.width-sw,box.height);},north:function(box){if(this.isCollapsed){return this.applyLayoutCollapsed(box);} +var sd=this.splitEl.dom,s=sd.style;var sh=sd.offsetHeight;this.panel.setPosition(box.x,box.y);s.left=(box.x)+'px';s.top=(box.y+box.height-sh)+'px';s.width=Math.max(0,box.width)+'px';this.panel.setSize(box.width,box.height-sh);},south:function(box){if(this.isCollapsed){return this.applyLayoutCollapsed(box);} +var sd=this.splitEl.dom,s=sd.style;var sh=sd.offsetHeight;this.panel.setPosition(box.x,box.y+sh);s.left=(box.x)+'px';s.top=(box.y)+'px';s.width=Math.max(0,box.width)+'px';this.panel.setSize(box.width,box.height-sh);}},render:function(ct,p){Ext.layout.BorderLayout.SplitRegion.superclass.render.call(this,ct,p);var ps=this.position;this.splitEl=ct.createChild({cls:"x-layout-split x-layout-split-"+ps,html:" ",id:this.panel.id+'-xsplit'});if(this.collapseMode=='mini'){this.miniSplitEl=this.splitEl.createChild({cls:"x-layout-mini x-layout-mini-"+ps,html:" "});this.miniSplitEl.addClassOnOver('x-layout-mini-over');this.miniSplitEl.on('click',this.onCollapseClick,this,{stopEvent:true});} +var s=this.splitSettings[ps];this.split=new Ext.SplitBar(this.splitEl.dom,p.el,s.orientation);this.split.tickSize=this.tickSize;this.split.placement=s.placement;this.split.getMaximumSize=this[s.maxFn].createDelegate(this);this.split.minSize=this.minSize||this[s.minProp];this.split.on("beforeapply",this.onSplitMove,this);this.split.useShim=this.useShim===true;this.maxSize=this.maxSize||this[s.maxProp];if(p.hidden){this.splitEl.hide();} +if(this.useSplitTips){this.splitEl.dom.title=this.collapsible?this.collapsibleSplitTip:this.splitTip;} +if(this.collapsible){this.splitEl.on("dblclick",this.onCollapseClick,this);}},getSize:function(){if(this.isCollapsed){return this.collapsedEl.getSize();} +var s=this.panel.getSize();if(this.position=='north'||this.position=='south'){s.height+=this.splitEl.dom.offsetHeight;}else{s.width+=this.splitEl.dom.offsetWidth;} +return s;},getHMaxSize:function(){var cmax=this.maxSize||10000;var center=this.layout.center;return Math.min(cmax,(this.el.getWidth()+center.el.getWidth())-center.getMinWidth());},getVMaxSize:function(){var cmax=this.maxSize||10000;var center=this.layout.center;return Math.min(cmax,(this.el.getHeight()+center.el.getHeight())-center.getMinHeight());},onSplitMove:function(split,newSize){var s=this.panel.getSize();this.lastSplitSize=newSize;if(this.position=='north'||this.position=='south'){this.panel.setSize(s.width,newSize);this.state.height=newSize;}else{this.panel.setSize(newSize,s.height);this.state.width=newSize;} +this.layout.layout();this.panel.saveState();return false;},getSplitBar:function(){return this.split;},destroy:function(){Ext.destroy(this.miniSplitEl,this.split,this.splitEl);Ext.layout.BorderLayout.SplitRegion.superclass.destroy.call(this);}});Ext.Container.LAYOUTS['border']=Ext.layout.BorderLayout;Ext.layout.FormLayout=Ext.extend(Ext.layout.AnchorLayout,{labelSeparator:':',trackLabels:true,type:'form',onRemove:function(c){Ext.layout.FormLayout.superclass.onRemove.call(this,c);if(this.trackLabels){c.un('show',this.onFieldShow,this);c.un('hide',this.onFieldHide,this);} +var el=c.getPositionEl(),ct=c.getItemCt&&c.getItemCt();if(c.rendered&&ct){if(el&&el.dom){el.insertAfter(ct);} +Ext.destroy(ct);Ext.destroyMembers(c,'label','itemCt');if(c.customItemCt){Ext.destroyMembers(c,'getItemCt','customItemCt');}}},setContainer:function(ct){Ext.layout.FormLayout.superclass.setContainer.call(this,ct);if(ct.labelAlign){ct.addClass('x-form-label-'+ct.labelAlign);} +if(ct.hideLabels){Ext.apply(this,{labelStyle:'display:none',elementStyle:'padding-left:0;',labelAdjust:0});}else{this.labelSeparator=Ext.isDefined(ct.labelSeparator)?ct.labelSeparator:this.labelSeparator;ct.labelWidth=ct.labelWidth||100;if(Ext.isNumber(ct.labelWidth)){var pad=Ext.isNumber(ct.labelPad)?ct.labelPad:5;Ext.apply(this,{labelAdjust:ct.labelWidth+pad,labelStyle:'width:'+ct.labelWidth+'px;',elementStyle:'padding-left:'+(ct.labelWidth+pad)+'px'});} +if(ct.labelAlign=='top'){Ext.apply(this,{labelStyle:'width:auto;',labelAdjust:0,elementStyle:'padding-left:0;'});}}},isHide:function(c){return c.hideLabel||this.container.hideLabels;},onFieldShow:function(c){c.getItemCt().removeClass('x-hide-'+c.hideMode);if(c.isComposite){c.doLayout();}},onFieldHide:function(c){c.getItemCt().addClass('x-hide-'+c.hideMode);},getLabelStyle:function(s){var ls='',items=[this.labelStyle,s];for(var i=0,len=items.length;i<len;++i){if(items[i]){ls+=items[i];if(ls.substr(-1,1)!=';'){ls+=';';}}} +return ls;},renderItem:function(c,position,target){if(c&&(c.isFormField||c.fieldLabel)&&c.inputType!='hidden'){var args=this.getTemplateArgs(c);if(Ext.isNumber(position)){position=target.dom.childNodes[position]||null;} +if(position){c.itemCt=this.fieldTpl.insertBefore(position,args,true);}else{c.itemCt=this.fieldTpl.append(target,args,true);} +if(!c.getItemCt){Ext.apply(c,{getItemCt:function(){return c.itemCt;},customItemCt:true});} +c.label=c.getItemCt().child('label.x-form-item-label');if(!c.rendered){c.render('x-form-el-'+c.id);}else if(!this.isValidParent(c,target)){Ext.fly('x-form-el-'+c.id).appendChild(c.getPositionEl());} +if(this.trackLabels){if(c.hidden){this.onFieldHide(c);} +c.on({scope:this,show:this.onFieldShow,hide:this.onFieldHide});} +this.configureItem(c);}else{Ext.layout.FormLayout.superclass.renderItem.apply(this,arguments);}},getTemplateArgs:function(field){var noLabelSep=!field.fieldLabel||field.hideLabel;return{id:field.id,label:field.fieldLabel,itemCls:(field.itemCls||this.container.itemCls||'')+(field.hideLabel?' x-hide-label':''),clearCls:field.clearCls||'x-form-clear-left',labelStyle:this.getLabelStyle(field.labelStyle),elementStyle:this.elementStyle||'',labelSeparator:noLabelSep?'':(Ext.isDefined(field.labelSeparator)?field.labelSeparator:this.labelSeparator)};},adjustWidthAnchor:function(value,c){if(c.label&&!this.isHide(c)&&(this.container.labelAlign!='top')){var adjust=Ext.isIE6||(Ext.isIE&&!Ext.isStrict);return value-this.labelAdjust+(adjust?-3:0);} +return value;},adjustHeightAnchor:function(value,c){if(c.label&&!this.isHide(c)&&(this.container.labelAlign=='top')){return value-c.label.getHeight();} +return value;},isValidParent:function(c,target){return target&&this.container.getEl().contains(c.getPositionEl());}});Ext.Container.LAYOUTS['form']=Ext.layout.FormLayout;Ext.layout.AccordionLayout=Ext.extend(Ext.layout.FitLayout,{fill:true,autoWidth:true,titleCollapse:true,hideCollapseTool:false,collapseFirst:false,animate:false,sequence:false,activeOnTop:false,type:'accordion',renderItem:function(c){if(this.animate===false){c.animCollapse=false;} +c.collapsible=true;if(this.autoWidth){c.autoWidth=true;} +if(this.titleCollapse){c.titleCollapse=true;} +if(this.hideCollapseTool){c.hideCollapseTool=true;} +if(this.collapseFirst!==undefined){c.collapseFirst=this.collapseFirst;} +if(!this.activeItem&&!c.collapsed){this.setActiveItem(c,true);}else if(this.activeItem&&this.activeItem!=c){c.collapsed=true;} +Ext.layout.AccordionLayout.superclass.renderItem.apply(this,arguments);c.header.addClass('x-accordion-hd');c.on('beforeexpand',this.beforeExpand,this);},onRemove:function(c){Ext.layout.AccordionLayout.superclass.onRemove.call(this,c);if(c.rendered){c.header.removeClass('x-accordion-hd');} +c.un('beforeexpand',this.beforeExpand,this);},beforeExpand:function(p,anim){var ai=this.activeItem;if(ai){if(this.sequence){delete this.activeItem;if(!ai.collapsed){ai.collapse({callback:function(){p.expand(anim||true);},scope:this});return false;}}else{ai.collapse(this.animate);}} +this.setActive(p);if(this.activeOnTop){p.el.dom.parentNode.insertBefore(p.el.dom,p.el.dom.parentNode.firstChild);} +this.layout();},setItemSize:function(item,size){if(this.fill&&item){var hh=0,i,ct=this.getRenderedItems(this.container),len=ct.length,p;for(i=0;i<len;i++){if((p=ct[i])!=item&&!p.hidden){hh+=p.header.getHeight();}};size.height-=hh;item.setSize(size);}},setActiveItem:function(item){this.setActive(item,true);},setActive:function(item,expand){var ai=this.activeItem;item=this.container.getComponent(item);if(ai!=item){if(item.rendered&&item.collapsed&&expand){item.expand();}else{if(ai){ai.fireEvent('deactivate',ai);} +this.activeItem=item;item.fireEvent('activate',item);}}}});Ext.Container.LAYOUTS.accordion=Ext.layout.AccordionLayout;Ext.layout.Accordion=Ext.layout.AccordionLayout;Ext.layout.TableLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:false,type:'table',targetCls:'x-table-layout-ct',tableAttrs:null,setContainer:function(ct){Ext.layout.TableLayout.superclass.setContainer.call(this,ct);this.currentRow=0;this.currentColumn=0;this.cells=[];},onLayout:function(ct,target){var cs=ct.items.items,len=cs.length,c,i;if(!this.table){target.addClass('x-table-layout-ct');this.table=target.createChild(Ext.apply({tag:'table',cls:'x-table-layout',cellspacing:0,cn:{tag:'tbody'}},this.tableAttrs),null,true);} +this.renderAll(ct,target);},getRow:function(index){var row=this.table.tBodies[0].childNodes[index];if(!row){row=document.createElement('tr');this.table.tBodies[0].appendChild(row);} +return row;},getNextCell:function(c){var cell=this.getNextNonSpan(this.currentColumn,this.currentRow);var curCol=this.currentColumn=cell[0],curRow=this.currentRow=cell[1];for(var rowIndex=curRow;rowIndex<curRow+(c.rowspan||1);rowIndex++){if(!this.cells[rowIndex]){this.cells[rowIndex]=[];} +for(var colIndex=curCol;colIndex<curCol+(c.colspan||1);colIndex++){this.cells[rowIndex][colIndex]=true;}} +var td=document.createElement('td');if(c.cellId){td.id=c.cellId;} +var cls='x-table-layout-cell';if(c.cellCls){cls+=' '+c.cellCls;} +td.className=cls;if(c.colspan){td.colSpan=c.colspan;} +if(c.rowspan){td.rowSpan=c.rowspan;} +this.getRow(curRow).appendChild(td);return td;},getNextNonSpan:function(colIndex,rowIndex){var cols=this.columns;while((cols&&colIndex>=cols)||(this.cells[rowIndex]&&this.cells[rowIndex][colIndex])){if(cols&&colIndex>=cols){rowIndex++;colIndex=0;}else{colIndex++;}} +return[colIndex,rowIndex];},renderItem:function(c,position,target){if(!this.table){this.table=target.createChild(Ext.apply({tag:'table',cls:'x-table-layout',cellspacing:0,cn:{tag:'tbody'}},this.tableAttrs),null,true);} +if(c&&!c.rendered){c.render(this.getNextCell(c));this.configureItem(c);}else if(c&&!this.isValidParent(c,target)){var container=this.getNextCell(c);container.insertBefore(c.getPositionEl().dom,null);c.container=Ext.get(container);this.configureItem(c);}},isValidParent:function(c,target){return c.getPositionEl().up('table',5).dom.parentNode===(target.dom||target);},destroy:function(){delete this.table;Ext.layout.TableLayout.superclass.destroy.call(this);}});Ext.Container.LAYOUTS['table']=Ext.layout.TableLayout;Ext.layout.AbsoluteLayout=Ext.extend(Ext.layout.AnchorLayout,{extraCls:'x-abs-layout-item',type:'absolute',onLayout:function(ct,target){target.position();this.paddingLeft=target.getPadding('l');this.paddingTop=target.getPadding('t');Ext.layout.AbsoluteLayout.superclass.onLayout.call(this,ct,target);},adjustWidthAnchor:function(value,comp){return value?value-comp.getPosition(true)[0]+this.paddingLeft:value;},adjustHeightAnchor:function(value,comp){return value?value-comp.getPosition(true)[1]+this.paddingTop:value;}});Ext.Container.LAYOUTS['absolute']=Ext.layout.AbsoluteLayout;Ext.layout.BoxLayout=Ext.extend(Ext.layout.ContainerLayout,{defaultMargins:{left:0,top:0,right:0,bottom:0},padding:'0',pack:'start',monitorResize:true,type:'box',scrollOffset:0,extraCls:'x-box-item',targetCls:'x-box-layout-ct',innerCls:'x-box-inner',constructor:function(config){Ext.layout.BoxLayout.superclass.constructor.call(this,config);if(Ext.isString(this.defaultMargins)){this.defaultMargins=this.parseMargins(this.defaultMargins);} +var handler=this.overflowHandler;if(typeof handler=='string'){handler={type:handler};} +var handlerType='none';if(handler&&handler.type!=undefined){handlerType=handler.type;} +var constructor=Ext.layout.boxOverflow[handlerType];if(constructor[this.type]){constructor=constructor[this.type];} +this.overflowHandler=new constructor(this,handler);},onLayout:function(container,target){Ext.layout.BoxLayout.superclass.onLayout.call(this,container,target);var tSize=this.getLayoutTargetSize(),items=this.getVisibleItems(container),calcs=this.calculateChildBoxes(items,tSize),boxes=calcs.boxes,meta=calcs.meta;if(tSize.width>0){var handler=this.overflowHandler,method=meta.tooNarrow?'handleOverflow':'clearOverflow';var results=handler[method](calcs,tSize);if(results){if(results.targetSize){tSize=results.targetSize;} +if(results.recalculate){items=this.getVisibleItems(container);calcs=this.calculateChildBoxes(items,tSize);boxes=calcs.boxes;}}} +this.layoutTargetLastSize=tSize;this.childBoxCache=calcs;this.updateInnerCtSize(tSize,calcs);this.updateChildBoxes(boxes);this.handleTargetOverflow(tSize,container,target);},updateChildBoxes:function(boxes){for(var i=0,length=boxes.length;i<length;i++){var box=boxes[i],comp=box.component;if(box.dirtySize){comp.setSize(box.width,box.height);} +if(isNaN(box.left)||isNaN(box.top)){continue;} +comp.setPosition(box.left,box.top);}},updateInnerCtSize:function(tSize,calcs){var align=this.align,padding=this.padding,width=tSize.width,height=tSize.height;if(this.type=='hbox'){var innerCtWidth=width,innerCtHeight=calcs.meta.maxHeight+padding.top+padding.bottom;if(align=='stretch'){innerCtHeight=height;}else if(align=='middle'){innerCtHeight=Math.max(height,innerCtHeight);}}else{var innerCtHeight=height,innerCtWidth=calcs.meta.maxWidth+padding.left+padding.right;if(align=='stretch'){innerCtWidth=width;}else if(align=='center'){innerCtWidth=Math.max(width,innerCtWidth);}} +this.innerCt.setSize(innerCtWidth||undefined,innerCtHeight||undefined);},handleTargetOverflow:function(previousTargetSize,container,target){var overflow=target.getStyle('overflow');if(overflow&&overflow!='hidden'&&!this.adjustmentPass){var newTargetSize=this.getLayoutTargetSize();if(newTargetSize.width!=previousTargetSize.width||newTargetSize.height!=previousTargetSize.height){this.adjustmentPass=true;this.onLayout(container,target);}} +delete this.adjustmentPass;},isValidParent:function(c,target){return this.innerCt&&c.getPositionEl().dom.parentNode==this.innerCt.dom;},getVisibleItems:function(ct){var ct=ct||this.container,t=ct.getLayoutTarget(),cti=ct.items.items,len=cti.length,i,c,items=[];for(i=0;i<len;i++){if((c=cti[i]).rendered&&this.isValidParent(c,t)&&c.hidden!==true&&c.collapsed!==true&&c.shouldLayout!==false){items.push(c);}} +return items;},renderAll:function(ct,target){if(!this.innerCt){this.innerCt=target.createChild({cls:this.innerCls});this.padding=this.parseMargins(this.padding);} +Ext.layout.BoxLayout.superclass.renderAll.call(this,ct,this.innerCt);},getLayoutTargetSize:function(){var target=this.container.getLayoutTarget(),ret;if(target){ret=target.getViewSize();if(Ext.isIE&&Ext.isStrict&&ret.width==0){ret=target.getStyleSize();} +ret.width-=target.getPadding('lr');ret.height-=target.getPadding('tb');} +return ret;},renderItem:function(c){if(Ext.isString(c.margins)){c.margins=this.parseMargins(c.margins);}else if(!c.margins){c.margins=this.defaultMargins;} +Ext.layout.BoxLayout.superclass.renderItem.apply(this,arguments);},destroy:function(){Ext.destroy(this.overflowHandler);Ext.layout.BoxLayout.superclass.destroy.apply(this,arguments);}});Ext.ns('Ext.layout.boxOverflow');Ext.layout.boxOverflow.None=Ext.extend(Object,{constructor:function(layout,config){this.layout=layout;Ext.apply(this,config||{});},handleOverflow:Ext.emptyFn,clearOverflow:Ext.emptyFn});Ext.layout.boxOverflow.none=Ext.layout.boxOverflow.None;Ext.layout.boxOverflow.Menu=Ext.extend(Ext.layout.boxOverflow.None,{afterCls:'x-strip-right',noItemsMenuText:'<div class="x-toolbar-no-items">(None)</div>',constructor:function(layout){Ext.layout.boxOverflow.Menu.superclass.constructor.apply(this,arguments);this.menuItems=[];},createInnerElements:function(){if(!this.afterCt){this.afterCt=this.layout.innerCt.insertSibling({cls:this.afterCls},'before');}},clearOverflow:function(calculations,targetSize){var newWidth=targetSize.width+(this.afterCt?this.afterCt.getWidth():0),items=this.menuItems;this.hideTrigger();for(var index=0,length=items.length;index<length;index++){items.pop().component.show();} +return{targetSize:{height:targetSize.height,width:newWidth}};},showTrigger:function(){this.createMenu();this.menuTrigger.show();},hideTrigger:function(){if(this.menuTrigger!=undefined){this.menuTrigger.hide();}},beforeMenuShow:function(menu){var items=this.menuItems,len=items.length,item,prev;var needsSep=function(group,item){return group.isXType('buttongroup')&&!(item instanceof Ext.Toolbar.Separator);};this.clearMenu();menu.removeAll();for(var i=0;i<len;i++){item=items[i].component;if(prev&&(needsSep(item,prev)||needsSep(prev,item))){menu.add('-');} +this.addComponentToMenu(menu,item);prev=item;} +if(menu.items.length<1){menu.add(this.noItemsMenuText);}},createMenuConfig:function(component,hideOnClick){var config=Ext.apply({},component.initialConfig),group=component.toggleGroup;Ext.copyTo(config,component,['iconCls','icon','itemId','disabled','handler','scope','menu']);Ext.apply(config,{text:component.overflowText||component.text,hideOnClick:hideOnClick});if(group||component.enableToggle){Ext.apply(config,{group:group,checked:component.pressed,listeners:{checkchange:function(item,checked){component.toggle(checked);}}});} +delete config.ownerCt;delete config.xtype;delete config.id;return config;},addComponentToMenu:function(menu,component){if(component instanceof Ext.Toolbar.Separator){menu.add('-');}else if(Ext.isFunction(component.isXType)){if(component.isXType('splitbutton')){menu.add(this.createMenuConfig(component,true));}else if(component.isXType('button')){menu.add(this.createMenuConfig(component,!component.menu));}else if(component.isXType('buttongroup')){component.items.each(function(item){this.addComponentToMenu(menu,item);},this);}}},clearMenu:function(){var menu=this.moreMenu;if(menu&&menu.items){menu.items.each(function(item){delete item.menu;});}},createMenu:function(){if(!this.menuTrigger){this.createInnerElements();this.menu=new Ext.menu.Menu({ownerCt:this.layout.container,listeners:{scope:this,beforeshow:this.beforeMenuShow}});this.menuTrigger=new Ext.Button({iconCls:'x-toolbar-more-icon',cls:'x-toolbar-more',menu:this.menu,renderTo:this.afterCt});}},destroy:function(){Ext.destroy(this.menu,this.menuTrigger);}});Ext.layout.boxOverflow.menu=Ext.layout.boxOverflow.Menu;Ext.layout.boxOverflow.HorizontalMenu=Ext.extend(Ext.layout.boxOverflow.Menu,{constructor:function(){Ext.layout.boxOverflow.HorizontalMenu.superclass.constructor.apply(this,arguments);var me=this,layout=me.layout,origFunction=layout.calculateChildBoxes;layout.calculateChildBoxes=function(visibleItems,targetSize){var calcs=origFunction.apply(layout,arguments),meta=calcs.meta,items=me.menuItems;var hiddenWidth=0;for(var index=0,length=items.length;index<length;index++){hiddenWidth+=items[index].width;} +meta.minimumWidth+=hiddenWidth;meta.tooNarrow=meta.minimumWidth>targetSize.width;return calcs;};},handleOverflow:function(calculations,targetSize){this.showTrigger();var newWidth=targetSize.width-this.afterCt.getWidth(),boxes=calculations.boxes,usedWidth=0,recalculate=false;for(var index=0,length=boxes.length;index<length;index++){usedWidth+=boxes[index].width;} +var spareWidth=newWidth-usedWidth,showCount=0;for(var index=0,length=this.menuItems.length;index<length;index++){var hidden=this.menuItems[index],comp=hidden.component,width=hidden.width;if(width<spareWidth){comp.show();spareWidth-=width;showCount++;recalculate=true;}else{break;}} +if(recalculate){this.menuItems=this.menuItems.slice(showCount);}else{for(var i=boxes.length-1;i>=0;i--){var item=boxes[i].component,right=boxes[i].left+boxes[i].width;if(right>=newWidth){this.menuItems.unshift({component:item,width:boxes[i].width});item.hide();}else{break;}}} +if(this.menuItems.length==0){this.hideTrigger();} +return{targetSize:{height:targetSize.height,width:newWidth},recalculate:recalculate};}});Ext.layout.boxOverflow.menu.hbox=Ext.layout.boxOverflow.HorizontalMenu;Ext.layout.boxOverflow.Scroller=Ext.extend(Ext.layout.boxOverflow.None,{animateScroll:true,scrollIncrement:100,wheelIncrement:3,scrollRepeatInterval:400,scrollDuration:0.4,beforeCls:'x-strip-left',afterCls:'x-strip-right',scrollerCls:'x-strip-scroller',beforeScrollerCls:'x-strip-scroller-left',afterScrollerCls:'x-strip-scroller-right',createWheelListener:function(){this.layout.innerCt.on({scope:this,mousewheel:function(e){e.stopEvent();this.scrollBy(e.getWheelDelta()*this.wheelIncrement*-1,false);}});},handleOverflow:function(calculations,targetSize){this.createInnerElements();this.showScrollers();},clearOverflow:function(){this.hideScrollers();},showScrollers:function(){this.createScrollers();this.beforeScroller.show();this.afterScroller.show();this.updateScrollButtons();},hideScrollers:function(){if(this.beforeScroller!=undefined){this.beforeScroller.hide();this.afterScroller.hide();}},createScrollers:function(){if(!this.beforeScroller&&!this.afterScroller){var before=this.beforeCt.createChild({cls:String.format("{0} {1} ",this.scrollerCls,this.beforeScrollerCls)});var after=this.afterCt.createChild({cls:String.format("{0} {1}",this.scrollerCls,this.afterScrollerCls)});before.addClassOnOver(this.beforeScrollerCls+'-hover');after.addClassOnOver(this.afterScrollerCls+'-hover');before.setVisibilityMode(Ext.Element.DISPLAY);after.setVisibilityMode(Ext.Element.DISPLAY);this.beforeRepeater=new Ext.util.ClickRepeater(before,{interval:this.scrollRepeatInterval,handler:this.scrollLeft,scope:this});this.afterRepeater=new Ext.util.ClickRepeater(after,{interval:this.scrollRepeatInterval,handler:this.scrollRight,scope:this});this.beforeScroller=before;this.afterScroller=after;}},destroy:function(){Ext.destroy(this.beforeScroller,this.afterScroller,this.beforeRepeater,this.afterRepeater,this.beforeCt,this.afterCt);},scrollBy:function(delta,animate){this.scrollTo(this.getScrollPosition()+delta,animate);},getItem:function(item){if(Ext.isString(item)){item=Ext.getCmp(item);}else if(Ext.isNumber(item)){item=this.items[item];} +return item;},getScrollAnim:function(){return{duration:this.scrollDuration,callback:this.updateScrollButtons,scope:this};},updateScrollButtons:function(){if(this.beforeScroller==undefined||this.afterScroller==undefined){return;} +var beforeMeth=this.atExtremeBefore()?'addClass':'removeClass',afterMeth=this.atExtremeAfter()?'addClass':'removeClass',beforeCls=this.beforeScrollerCls+'-disabled',afterCls=this.afterScrollerCls+'-disabled';this.beforeScroller[beforeMeth](beforeCls);this.afterScroller[afterMeth](afterCls);this.scrolling=false;},atExtremeBefore:function(){return this.getScrollPosition()===0;},scrollLeft:function(animate){this.scrollBy(-this.scrollIncrement,animate);},scrollRight:function(animate){this.scrollBy(this.scrollIncrement,animate);},scrollToItem:function(item,animate){item=this.getItem(item);if(item!=undefined){var visibility=this.getItemVisibility(item);if(!visibility.fullyVisible){var box=item.getBox(true,true),newX=box.x;if(visibility.hiddenRight){newX-=(this.layout.innerCt.getWidth()-box.width);} +this.scrollTo(newX,animate);}}},getItemVisibility:function(item){var box=this.getItem(item).getBox(true,true),itemLeft=box.x,itemRight=box.x+box.width,scrollLeft=this.getScrollPosition(),scrollRight=this.layout.innerCt.getWidth()+scrollLeft;return{hiddenLeft:itemLeft<scrollLeft,hiddenRight:itemRight>scrollRight,fullyVisible:itemLeft>scrollLeft&&itemRight<scrollRight};}});Ext.layout.boxOverflow.scroller=Ext.layout.boxOverflow.Scroller;Ext.layout.boxOverflow.VerticalScroller=Ext.extend(Ext.layout.boxOverflow.Scroller,{scrollIncrement:75,wheelIncrement:2,handleOverflow:function(calculations,targetSize){Ext.layout.boxOverflow.VerticalScroller.superclass.handleOverflow.apply(this,arguments);return{targetSize:{height:targetSize.height-(this.beforeCt.getHeight()+this.afterCt.getHeight()),width:targetSize.width}};},createInnerElements:function(){var target=this.layout.innerCt;if(!this.beforeCt){this.beforeCt=target.insertSibling({cls:this.beforeCls},'before');this.afterCt=target.insertSibling({cls:this.afterCls},'after');this.createWheelListener();}},scrollTo:function(position,animate){var oldPosition=this.getScrollPosition(),newPosition=position.constrain(0,this.getMaxScrollBottom());if(newPosition!=oldPosition&&!this.scrolling){if(animate==undefined){animate=this.animateScroll;} +this.layout.innerCt.scrollTo('top',newPosition,animate?this.getScrollAnim():false);if(animate){this.scrolling=true;}else{this.scrolling=false;this.updateScrollButtons();}}},getScrollPosition:function(){return parseInt(this.layout.innerCt.dom.scrollTop,10)||0;},getMaxScrollBottom:function(){return this.layout.innerCt.dom.scrollHeight-this.layout.innerCt.getHeight();},atExtremeAfter:function(){return this.getScrollPosition()>=this.getMaxScrollBottom();}});Ext.layout.boxOverflow.scroller.vbox=Ext.layout.boxOverflow.VerticalScroller;Ext.layout.boxOverflow.HorizontalScroller=Ext.extend(Ext.layout.boxOverflow.Scroller,{handleOverflow:function(calculations,targetSize){Ext.layout.boxOverflow.HorizontalScroller.superclass.handleOverflow.apply(this,arguments);return{targetSize:{height:targetSize.height,width:targetSize.width-(this.beforeCt.getWidth()+this.afterCt.getWidth())}};},createInnerElements:function(){var target=this.layout.innerCt;if(!this.beforeCt){this.afterCt=target.insertSibling({cls:this.afterCls},'before');this.beforeCt=target.insertSibling({cls:this.beforeCls},'before');this.createWheelListener();}},scrollTo:function(position,animate){var oldPosition=this.getScrollPosition(),newPosition=position.constrain(0,this.getMaxScrollRight());if(newPosition!=oldPosition&&!this.scrolling){if(animate==undefined){animate=this.animateScroll;} +this.layout.innerCt.scrollTo('left',newPosition,animate?this.getScrollAnim():false);if(animate){this.scrolling=true;}else{this.scrolling=false;this.updateScrollButtons();}}},getScrollPosition:function(){return parseInt(this.layout.innerCt.dom.scrollLeft,10)||0;},getMaxScrollRight:function(){return this.layout.innerCt.dom.scrollWidth-this.layout.innerCt.getWidth();},atExtremeAfter:function(){return this.getScrollPosition()>=this.getMaxScrollRight();}});Ext.layout.boxOverflow.scroller.hbox=Ext.layout.boxOverflow.HorizontalScroller;Ext.layout.HBoxLayout=Ext.extend(Ext.layout.BoxLayout,{align:'top',type:'hbox',calculateChildBoxes:function(visibleItems,targetSize){var visibleCount=visibleItems.length,padding=this.padding,topOffset=padding.top,leftOffset=padding.left,paddingVert=topOffset+padding.bottom,paddingHoriz=leftOffset+padding.right,width=targetSize.width-this.scrollOffset,height=targetSize.height,availHeight=Math.max(0,height-paddingVert),isStart=this.pack=='start',isCenter=this.pack=='center',isEnd=this.pack=='end',nonFlexWidth=0,maxHeight=0,totalFlex=0,desiredWidth=0,minimumWidth=0,boxes=[],child,childWidth,childHeight,childSize,childMargins,canLayout,i,calcs,flexedWidth,horizMargins,vertMargins,stretchHeight;for(i=0;i<visibleCount;i++){child=visibleItems[i];childHeight=child.height;childWidth=child.width;canLayout=!child.hasLayout&&typeof child.doLayout=='function';if(typeof childWidth!='number'){if(child.flex&&!childWidth){totalFlex+=child.flex;}else{if(!childWidth&&canLayout){child.doLayout();} +childSize=child.getSize();childWidth=childSize.width;childHeight=childSize.height;}} +childMargins=child.margins;horizMargins=childMargins.left+childMargins.right;nonFlexWidth+=horizMargins+(childWidth||0);desiredWidth+=horizMargins+(child.flex?child.minWidth||0:childWidth);minimumWidth+=horizMargins+(child.minWidth||childWidth||0);if(typeof childHeight!='number'){if(canLayout){child.doLayout();} +childHeight=child.getHeight();} +maxHeight=Math.max(maxHeight,childHeight+childMargins.top+childMargins.bottom);boxes.push({component:child,height:childHeight||undefined,width:childWidth||undefined});} +var shortfall=desiredWidth-width,tooNarrow=minimumWidth>width;var availableWidth=Math.max(0,width-nonFlexWidth-paddingHoriz);if(tooNarrow){for(i=0;i<visibleCount;i++){boxes[i].width=visibleItems[i].minWidth||visibleItems[i].width||boxes[i].width;}}else{if(shortfall>0){var minWidths=[];for(var index=0,length=visibleCount;index<length;index++){var item=visibleItems[index],minWidth=item.minWidth||0;if(item.flex){boxes[index].width=minWidth;}else{minWidths.push({minWidth:minWidth,available:boxes[index].width-minWidth,index:index});}} +minWidths.sort(function(a,b){return a.available>b.available?1:-1;});for(var i=0,length=minWidths.length;i<length;i++){var itemIndex=minWidths[i].index;if(itemIndex==undefined){continue;} +var item=visibleItems[itemIndex],box=boxes[itemIndex],oldWidth=box.width,minWidth=item.minWidth,newWidth=Math.max(minWidth,oldWidth-Math.ceil(shortfall/(length-i))),reduction=oldWidth-newWidth;boxes[itemIndex].width=newWidth;shortfall-=reduction;}}else{var remainingWidth=availableWidth,remainingFlex=totalFlex;for(i=0;i<visibleCount;i++){child=visibleItems[i];calcs=boxes[i];childMargins=child.margins;vertMargins=childMargins.top+childMargins.bottom;if(isStart&&child.flex&&!child.width){flexedWidth=Math.ceil((child.flex/remainingFlex)*remainingWidth);remainingWidth-=flexedWidth;remainingFlex-=child.flex;calcs.width=flexedWidth;calcs.dirtySize=true;}}}} +if(isCenter){leftOffset+=availableWidth/2;}else if(isEnd){leftOffset+=availableWidth;} +for(i=0;i<visibleCount;i++){child=visibleItems[i];calcs=boxes[i];childMargins=child.margins;leftOffset+=childMargins.left;vertMargins=childMargins.top+childMargins.bottom;calcs.left=leftOffset;calcs.top=topOffset+childMargins.top;switch(this.align){case'stretch':stretchHeight=availHeight-vertMargins;calcs.height=stretchHeight.constrain(child.minHeight||0,child.maxHeight||1000000);calcs.dirtySize=true;break;case'stretchmax':stretchHeight=maxHeight-vertMargins;calcs.height=stretchHeight.constrain(child.minHeight||0,child.maxHeight||1000000);calcs.dirtySize=true;break;case'middle':var diff=availHeight-calcs.height-vertMargins;if(diff>0){calcs.top=topOffset+vertMargins+(diff/2);}} +leftOffset+=calcs.width+childMargins.right;} +return{boxes:boxes,meta:{maxHeight:maxHeight,nonFlexWidth:nonFlexWidth,desiredWidth:desiredWidth,minimumWidth:minimumWidth,shortfall:desiredWidth-width,tooNarrow:tooNarrow}};}});Ext.Container.LAYOUTS.hbox=Ext.layout.HBoxLayout;Ext.layout.VBoxLayout=Ext.extend(Ext.layout.BoxLayout,{align:'left',type:'vbox',calculateChildBoxes:function(visibleItems,targetSize){var visibleCount=visibleItems.length,padding=this.padding,topOffset=padding.top,leftOffset=padding.left,paddingVert=topOffset+padding.bottom,paddingHoriz=leftOffset+padding.right,width=targetSize.width-this.scrollOffset,height=targetSize.height,availWidth=Math.max(0,width-paddingHoriz),isStart=this.pack=='start',isCenter=this.pack=='center',isEnd=this.pack=='end',nonFlexHeight=0,maxWidth=0,totalFlex=0,desiredHeight=0,minimumHeight=0,boxes=[],child,childWidth,childHeight,childSize,childMargins,canLayout,i,calcs,flexedWidth,horizMargins,vertMargins,stretchWidth;for(i=0;i<visibleCount;i++){child=visibleItems[i];childHeight=child.height;childWidth=child.width;canLayout=!child.hasLayout&&typeof child.doLayout=='function';if(typeof childHeight!='number'){if(child.flex&&!childHeight){totalFlex+=child.flex;}else{if(!childHeight&&canLayout){child.doLayout();} +childSize=child.getSize();childWidth=childSize.width;childHeight=childSize.height;}} +childMargins=child.margins;vertMargins=childMargins.top+childMargins.bottom;nonFlexHeight+=vertMargins+(childHeight||0);desiredHeight+=vertMargins+(child.flex?child.minHeight||0:childHeight);minimumHeight+=vertMargins+(child.minHeight||childHeight||0);if(typeof childWidth!='number'){if(canLayout){child.doLayout();} +childWidth=child.getWidth();} +maxWidth=Math.max(maxWidth,childWidth+childMargins.left+childMargins.right);boxes.push({component:child,height:childHeight||undefined,width:childWidth||undefined});} +var shortfall=desiredHeight-height,tooNarrow=minimumHeight>height;var availableHeight=Math.max(0,(height-nonFlexHeight-paddingVert));if(tooNarrow){for(i=0,length=visibleCount;i<length;i++){boxes[i].height=visibleItems[i].minHeight||visibleItems[i].height||boxes[i].height;}}else{if(shortfall>0){var minHeights=[];for(var index=0,length=visibleCount;index<length;index++){var item=visibleItems[index],minHeight=item.minHeight||0;if(item.flex){boxes[index].height=minHeight;}else{minHeights.push({minHeight:minHeight,available:boxes[index].height-minHeight,index:index});}} +minHeights.sort(function(a,b){return a.available>b.available?1:-1;});for(var i=0,length=minHeights.length;i<length;i++){var itemIndex=minHeights[i].index;if(itemIndex==undefined){continue;} +var item=visibleItems[itemIndex],box=boxes[itemIndex],oldHeight=box.height,minHeight=item.minHeight,newHeight=Math.max(minHeight,oldHeight-Math.ceil(shortfall/(length-i))),reduction=oldHeight-newHeight;boxes[itemIndex].height=newHeight;shortfall-=reduction;}}else{var remainingHeight=availableHeight,remainingFlex=totalFlex;for(i=0;i<visibleCount;i++){child=visibleItems[i];calcs=boxes[i];childMargins=child.margins;horizMargins=childMargins.left+childMargins.right;if(isStart&&child.flex&&!child.height){flexedHeight=Math.ceil((child.flex/remainingFlex)*remainingHeight);remainingHeight-=flexedHeight;remainingFlex-=child.flex;calcs.height=flexedHeight;calcs.dirtySize=true;}}}} +if(isCenter){topOffset+=availableHeight/2;}else if(isEnd){topOffset+=availableHeight;} +for(i=0;i<visibleCount;i++){child=visibleItems[i];calcs=boxes[i];childMargins=child.margins;topOffset+=childMargins.top;horizMargins=childMargins.left+childMargins.right;calcs.left=leftOffset+childMargins.left;calcs.top=topOffset;switch(this.align){case'stretch':stretchWidth=availWidth-horizMargins;calcs.width=stretchWidth.constrain(child.minWidth||0,child.maxWidth||1000000);calcs.dirtySize=true;break;case'stretchmax':stretchWidth=maxWidth-horizMargins;calcs.width=stretchWidth.constrain(child.minWidth||0,child.maxWidth||1000000);calcs.dirtySize=true;break;case'center':var diff=availWidth-calcs.width-horizMargins;if(diff>0){calcs.left=leftOffset+horizMargins+(diff/2);}} +topOffset+=calcs.height+childMargins.bottom;} +return{boxes:boxes,meta:{maxWidth:maxWidth,nonFlexHeight:nonFlexHeight,desiredHeight:desiredHeight,minimumHeight:minimumHeight,shortfall:desiredHeight-height,tooNarrow:tooNarrow}};}});Ext.Container.LAYOUTS.vbox=Ext.layout.VBoxLayout;Ext.layout.ToolbarLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:'toolbar',triggerWidth:18,noItemsMenuText:'<div class="x-toolbar-no-items">(None)</div>',lastOverflow:false,tableHTML:['<table cellspacing="0" class="x-toolbar-ct">','<tbody>','<tr>','<td class="x-toolbar-left" align="{0}">','<table cellspacing="0">','<tbody>','<tr class="x-toolbar-left-row"></tr>','</tbody>','</table>','</td>','<td class="x-toolbar-right" align="right">','<table cellspacing="0" class="x-toolbar-right-ct">','<tbody>','<tr>','<td>','<table cellspacing="0">','<tbody>','<tr class="x-toolbar-right-row"></tr>','</tbody>','</table>','</td>','<td>','<table cellspacing="0">','<tbody>','<tr class="x-toolbar-extras-row"></tr>','</tbody>','</table>','</td>','</tr>','</tbody>','</table>','</td>','</tr>','</tbody>','</table>'].join(""),onLayout:function(ct,target){if(!this.leftTr){var align=ct.buttonAlign=='center'?'center':'left';target.addClass('x-toolbar-layout-ct');target.insertHtml('beforeEnd',String.format(this.tableHTML,align));this.leftTr=target.child('tr.x-toolbar-left-row',true);this.rightTr=target.child('tr.x-toolbar-right-row',true);this.extrasTr=target.child('tr.x-toolbar-extras-row',true);if(this.hiddenItem==undefined){this.hiddenItems=[];}} +var side=ct.buttonAlign=='right'?this.rightTr:this.leftTr,items=ct.items.items,position=0;for(var i=0,len=items.length,c;i<len;i++,position++){c=items[i];if(c.isFill){side=this.rightTr;position=-1;}else if(!c.rendered){c.render(this.insertCell(c,side,position));this.configureItem(c);}else{if(!c.xtbHidden&&!this.isValidParent(c,side.childNodes[position])){var td=this.insertCell(c,side,position);td.appendChild(c.getPositionEl().dom);c.container=Ext.get(td);}}} +this.cleanup(this.leftTr);this.cleanup(this.rightTr);this.cleanup(this.extrasTr);this.fitToSize(target);},cleanup:function(el){var cn=el.childNodes,i,c;for(i=cn.length-1;i>=0&&(c=cn[i]);i--){if(!c.firstChild){el.removeChild(c);}}},insertCell:function(c,target,position){var td=document.createElement('td');td.className='x-toolbar-cell';target.insertBefore(td,target.childNodes[position]||null);return td;},hideItem:function(item){this.hiddenItems.push(item);item.xtbHidden=true;item.xtbWidth=item.getPositionEl().dom.parentNode.offsetWidth;item.hide();},unhideItem:function(item){item.show();item.xtbHidden=false;this.hiddenItems.remove(item);},getItemWidth:function(c){return c.hidden?(c.xtbWidth||0):c.getPositionEl().dom.parentNode.offsetWidth;},fitToSize:function(target){if(this.container.enableOverflow===false){return;} +var width=target.dom.clientWidth,tableWidth=target.dom.firstChild.offsetWidth,clipWidth=width-this.triggerWidth,lastWidth=this.lastWidth||0,hiddenItems=this.hiddenItems,hasHiddens=hiddenItems.length!=0,isLarger=width>=lastWidth;this.lastWidth=width;if(tableWidth>width||(hasHiddens&&isLarger)){var items=this.container.items.items,len=items.length,loopWidth=0,item;for(var i=0;i<len;i++){item=items[i];if(!item.isFill){loopWidth+=this.getItemWidth(item);if(loopWidth>clipWidth){if(!(item.hidden||item.xtbHidden)){this.hideItem(item);}}else if(item.xtbHidden){this.unhideItem(item);}}}} +hasHiddens=hiddenItems.length!=0;if(hasHiddens){this.initMore();if(!this.lastOverflow){this.container.fireEvent('overflowchange',this.container,true);this.lastOverflow=true;}}else if(this.more){this.clearMenu();this.more.destroy();delete this.more;if(this.lastOverflow){this.container.fireEvent('overflowchange',this.container,false);this.lastOverflow=false;}}},createMenuConfig:function(component,hideOnClick){var config=Ext.apply({},component.initialConfig),group=component.toggleGroup;Ext.copyTo(config,component,['iconCls','icon','itemId','disabled','handler','scope','menu']);Ext.apply(config,{text:component.overflowText||component.text,hideOnClick:hideOnClick});if(group||component.enableToggle){Ext.apply(config,{group:group,checked:component.pressed,listeners:{checkchange:function(item,checked){component.toggle(checked);}}});} +delete config.ownerCt;delete config.xtype;delete config.id;return config;},addComponentToMenu:function(menu,component){if(component instanceof Ext.Toolbar.Separator){menu.add('-');}else if(Ext.isFunction(component.isXType)){if(component.isXType('splitbutton')){menu.add(this.createMenuConfig(component,true));}else if(component.isXType('button')){menu.add(this.createMenuConfig(component,!component.menu));}else if(component.isXType('buttongroup')){component.items.each(function(item){this.addComponentToMenu(menu,item);},this);}}},clearMenu:function(){var menu=this.moreMenu;if(menu&&menu.items){menu.items.each(function(item){delete item.menu;});}},beforeMoreShow:function(menu){var items=this.container.items.items,len=items.length,item,prev;var needsSep=function(group,item){return group.isXType('buttongroup')&&!(item instanceof Ext.Toolbar.Separator);};this.clearMenu();menu.removeAll();for(var i=0;i<len;i++){item=items[i];if(item.xtbHidden){if(prev&&(needsSep(item,prev)||needsSep(prev,item))){menu.add('-');} +this.addComponentToMenu(menu,item);prev=item;}} +if(menu.items.length<1){menu.add(this.noItemsMenuText);}},initMore:function(){if(!this.more){this.moreMenu=new Ext.menu.Menu({ownerCt:this.container,listeners:{beforeshow:this.beforeMoreShow,scope:this}});this.more=new Ext.Button({iconCls:'x-toolbar-more-icon',cls:'x-toolbar-more',menu:this.moreMenu,ownerCt:this.container});var td=this.insertCell(this.more,this.extrasTr,100);this.more.render(td);}},destroy:function(){Ext.destroy(this.more,this.moreMenu);delete this.leftTr;delete this.rightTr;delete this.extrasTr;Ext.layout.ToolbarLayout.superclass.destroy.call(this);}});Ext.Container.LAYOUTS.toolbar=Ext.layout.ToolbarLayout;Ext.layout.MenuLayout=Ext.extend(Ext.layout.ContainerLayout,{monitorResize:true,type:'menu',setContainer:function(ct){this.monitorResize=!ct.floating;ct.on('autosize',this.doAutoSize,this);Ext.layout.MenuLayout.superclass.setContainer.call(this,ct);},renderItem:function(c,position,target){if(!this.itemTpl){this.itemTpl=Ext.layout.MenuLayout.prototype.itemTpl=new Ext.XTemplate('<li id="{itemId}" class="{itemCls}">','<tpl if="needsIcon">','<img alt="{altText}" src="{icon}" class="{iconCls}"/>','</tpl>','</li>');} +if(c&&!c.rendered){if(Ext.isNumber(position)){position=target.dom.childNodes[position];} +var a=this.getItemArgs(c);c.render(c.positionEl=position?this.itemTpl.insertBefore(position,a,true):this.itemTpl.append(target,a,true));c.positionEl.menuItemId=c.getItemId();if(!a.isMenuItem&&a.needsIcon){c.positionEl.addClass('x-menu-list-item-indent');} +this.configureItem(c);}else if(c&&!this.isValidParent(c,target)){if(Ext.isNumber(position)){position=target.dom.childNodes[position];} +target.dom.insertBefore(c.getActionEl().dom,position||null);}},getItemArgs:function(c){var isMenuItem=c instanceof Ext.menu.Item,canHaveIcon=!(isMenuItem||c instanceof Ext.menu.Separator);return{isMenuItem:isMenuItem,needsIcon:canHaveIcon&&(c.icon||c.iconCls),icon:c.icon||Ext.BLANK_IMAGE_URL,iconCls:'x-menu-item-icon '+(c.iconCls||''),itemId:'x-menu-el-'+c.id,itemCls:'x-menu-list-item ',altText:c.altText||''};},isValidParent:function(c,target){return c.el.up('li.x-menu-list-item',5).dom.parentNode===(target.dom||target);},onLayout:function(ct,target){Ext.layout.MenuLayout.superclass.onLayout.call(this,ct,target);this.doAutoSize();},doAutoSize:function(){var ct=this.container,w=ct.width;if(ct.floating){if(w){ct.setWidth(w);}else if(Ext.isIE){ct.setWidth(Ext.isStrict&&(Ext.isIE7||Ext.isIE8)?'auto':ct.minWidth);var el=ct.getEl(),t=el.dom.offsetWidth;ct.setWidth(ct.getLayoutTarget().getWidth()+el.getFrameWidth('lr'));}}}});Ext.Container.LAYOUTS['menu']=Ext.layout.MenuLayout;Ext.Viewport=Ext.extend(Ext.Container,{initComponent:function(){Ext.Viewport.superclass.initComponent.call(this);document.getElementsByTagName('html')[0].className+=' x-viewport';this.el=Ext.getBody();this.el.setHeight=Ext.emptyFn;this.el.setWidth=Ext.emptyFn;this.el.setSize=Ext.emptyFn;this.el.dom.scroll='no';this.allowDomMove=false;this.autoWidth=true;this.autoHeight=true;Ext.EventManager.onWindowResize(this.fireResize,this);this.renderTo=this.el;},fireResize:function(w,h){this.fireEvent('resize',this,w,h,w,h);}});Ext.reg('viewport',Ext.Viewport);Ext.Panel=Ext.extend(Ext.Container,{baseCls:'x-panel',collapsedCls:'x-panel-collapsed',maskDisabled:true,animCollapse:Ext.enableFx,headerAsText:true,buttonAlign:'right',collapsed:false,collapseFirst:true,minButtonWidth:75,elements:'body',preventBodyReset:false,padding:undefined,resizeEvent:'bodyresize',toolTarget:'header',collapseEl:'bwrap',slideAnchor:'t',disabledClass:'',deferHeight:true,expandDefaults:{duration:0.25},collapseDefaults:{duration:0.25},initComponent:function(){Ext.Panel.superclass.initComponent.call(this);this.addEvents('bodyresize','titlechange','iconchange','collapse','expand','beforecollapse','beforeexpand','beforeclose','close','activate','deactivate');if(this.unstyled){this.baseCls='x-plain';} +this.toolbars=[];if(this.tbar){this.elements+=',tbar';this.topToolbar=this.createToolbar(this.tbar);this.tbar=null;} +if(this.bbar){this.elements+=',bbar';this.bottomToolbar=this.createToolbar(this.bbar);this.bbar=null;} +if(this.header===true){this.elements+=',header';this.header=null;}else if(this.headerCfg||(this.title&&this.header!==false)){this.elements+=',header';} +if(this.footerCfg||this.footer===true){this.elements+=',footer';this.footer=null;} +if(this.buttons){this.fbar=this.buttons;this.buttons=null;} +if(this.fbar){this.createFbar(this.fbar);} +if(this.autoLoad){this.on('render',this.doAutoLoad,this,{delay:10});}},createFbar:function(fbar){var min=this.minButtonWidth;this.elements+=',footer';this.fbar=this.createToolbar(fbar,{buttonAlign:this.buttonAlign,toolbarCls:'x-panel-fbar',enableOverflow:false,defaults:function(c){return{minWidth:c.minWidth||min};}});this.fbar.items.each(function(c){c.minWidth=c.minWidth||this.minButtonWidth;},this);this.buttons=this.fbar.items.items;},createToolbar:function(tb,options){var result;if(Ext.isArray(tb)){tb={items:tb};} +result=tb.events?Ext.apply(tb,options):this.createComponent(Ext.apply({},tb,options),'toolbar');this.toolbars.push(result);return result;},createElement:function(name,pnode){if(this[name]){pnode.appendChild(this[name].dom);return;} +if(name==='bwrap'||this.elements.indexOf(name)!=-1){if(this[name+'Cfg']){this[name]=Ext.fly(pnode).createChild(this[name+'Cfg']);}else{var el=document.createElement('div');el.className=this[name+'Cls'];this[name]=Ext.get(pnode.appendChild(el));} +if(this[name+'CssClass']){this[name].addClass(this[name+'CssClass']);} +if(this[name+'Style']){this[name].applyStyles(this[name+'Style']);}}},onRender:function(ct,position){Ext.Panel.superclass.onRender.call(this,ct,position);this.createClasses();var el=this.el,d=el.dom,bw,ts;if(this.collapsible&&!this.hideCollapseTool){this.tools=this.tools?this.tools.slice(0):[];this.tools[this.collapseFirst?'unshift':'push']({id:'toggle',handler:this.toggleCollapse,scope:this});} +if(this.tools){ts=this.tools;this.elements+=(this.header!==false)?',header':'';} +this.tools={};el.addClass(this.baseCls);if(d.firstChild){this.header=el.down('.'+this.headerCls);this.bwrap=el.down('.'+this.bwrapCls);var cp=this.bwrap?this.bwrap:el;this.tbar=cp.down('.'+this.tbarCls);this.body=cp.down('.'+this.bodyCls);this.bbar=cp.down('.'+this.bbarCls);this.footer=cp.down('.'+this.footerCls);this.fromMarkup=true;} +if(this.preventBodyReset===true){el.addClass('x-panel-reset');} +if(this.cls){el.addClass(this.cls);} +if(this.buttons){this.elements+=',footer';} +if(this.frame){el.insertHtml('afterBegin',String.format(Ext.Element.boxMarkup,this.baseCls));this.createElement('header',d.firstChild.firstChild.firstChild);this.createElement('bwrap',d);bw=this.bwrap.dom;var ml=d.childNodes[1],bl=d.childNodes[2];bw.appendChild(ml);bw.appendChild(bl);var mc=bw.firstChild.firstChild.firstChild;this.createElement('tbar',mc);this.createElement('body',mc);this.createElement('bbar',mc);this.createElement('footer',bw.lastChild.firstChild.firstChild);if(!this.footer){this.bwrap.dom.lastChild.className+=' x-panel-nofooter';} +this.ft=Ext.get(this.bwrap.dom.lastChild);this.mc=Ext.get(mc);}else{this.createElement('header',d);this.createElement('bwrap',d);bw=this.bwrap.dom;this.createElement('tbar',bw);this.createElement('body',bw);this.createElement('bbar',bw);this.createElement('footer',bw);if(!this.header){this.body.addClass(this.bodyCls+'-noheader');if(this.tbar){this.tbar.addClass(this.tbarCls+'-noheader');}}} +if(Ext.isDefined(this.padding)){this.body.setStyle('padding',this.body.addUnits(this.padding));} +if(this.border===false){this.el.addClass(this.baseCls+'-noborder');this.body.addClass(this.bodyCls+'-noborder');if(this.header){this.header.addClass(this.headerCls+'-noborder');} +if(this.footer){this.footer.addClass(this.footerCls+'-noborder');} +if(this.tbar){this.tbar.addClass(this.tbarCls+'-noborder');} +if(this.bbar){this.bbar.addClass(this.bbarCls+'-noborder');}} +if(this.bodyBorder===false){this.body.addClass(this.bodyCls+'-noborder');} +this.bwrap.enableDisplayMode('block');if(this.header){this.header.unselectable();if(this.headerAsText){this.header.dom.innerHTML='<span class="'+this.headerTextCls+'">'+this.header.dom.innerHTML+'</span>';if(this.iconCls){this.setIconClass(this.iconCls);}}} +if(this.floating){this.makeFloating(this.floating);} +if(this.collapsible&&this.titleCollapse&&this.header){this.mon(this.header,'click',this.toggleCollapse,this);this.header.setStyle('cursor','pointer');} +if(ts){this.addTool.apply(this,ts);} +if(this.fbar){this.footer.addClass('x-panel-btns');this.fbar.ownerCt=this;this.fbar.render(this.footer);this.footer.createChild({cls:'x-clear'});} +if(this.tbar&&this.topToolbar){this.topToolbar.ownerCt=this;this.topToolbar.render(this.tbar);} +if(this.bbar&&this.bottomToolbar){this.bottomToolbar.ownerCt=this;this.bottomToolbar.render(this.bbar);}},setIconClass:function(cls){var old=this.iconCls;this.iconCls=cls;if(this.rendered&&this.header){if(this.frame){this.header.addClass('x-panel-icon');this.header.replaceClass(old,this.iconCls);}else{var hd=this.header,img=hd.child('img.x-panel-inline-icon');if(img){Ext.fly(img).replaceClass(old,this.iconCls);}else{var hdspan=hd.child('span.'+this.headerTextCls);if(hdspan){Ext.DomHelper.insertBefore(hdspan.dom,{tag:'img',alt:'',src:Ext.BLANK_IMAGE_URL,cls:'x-panel-inline-icon '+this.iconCls});}}}} +this.fireEvent('iconchange',this,cls,old);},makeFloating:function(cfg){this.floating=true;this.el=new Ext.Layer(Ext.apply({},cfg,{shadow:Ext.isDefined(this.shadow)?this.shadow:'sides',shadowOffset:this.shadowOffset,constrain:false,shim:this.shim===false?false:undefined}),this.el);},getTopToolbar:function(){return this.topToolbar;},getBottomToolbar:function(){return this.bottomToolbar;},getFooterToolbar:function(){return this.fbar;},addButton:function(config,handler,scope){if(!this.fbar){this.createFbar([]);} +if(handler){if(Ext.isString(config)){config={text:config};} +config=Ext.apply({handler:handler,scope:scope},config);} +return this.fbar.add(config);},addTool:function(){if(!this.rendered){if(!this.tools){this.tools=[];} +Ext.each(arguments,function(arg){this.tools.push(arg);},this);return;} +if(!this[this.toolTarget]){return;} +if(!this.toolTemplate){var tt=new Ext.Template('<div class="x-tool x-tool-{id}"> </div>');tt.disableFormats=true;tt.compile();Ext.Panel.prototype.toolTemplate=tt;} +for(var i=0,a=arguments,len=a.length;i<len;i++){var tc=a[i];if(!this.tools[tc.id]){var overCls='x-tool-'+tc.id+'-over';var t=this.toolTemplate.insertFirst(this[this.toolTarget],tc,true);this.tools[tc.id]=t;t.enableDisplayMode('block');this.mon(t,'click',this.createToolHandler(t,tc,overCls,this));if(tc.on){this.mon(t,tc.on);} +if(tc.hidden){t.hide();} +if(tc.qtip){if(Ext.isObject(tc.qtip)){Ext.QuickTips.register(Ext.apply({target:t.id},tc.qtip));}else{t.dom.qtip=tc.qtip;}} +t.addClassOnOver(overCls);}}},onLayout:function(shallow,force){Ext.Panel.superclass.onLayout.apply(this,arguments);if(this.hasLayout&&this.toolbars.length>0){Ext.each(this.toolbars,function(tb){tb.doLayout(undefined,force);});this.syncHeight();}},syncHeight:function(){var h=this.toolbarHeight,bd=this.body,lsh=this.lastSize.height,sz;if(this.autoHeight||!Ext.isDefined(lsh)||lsh=='auto'){return;} +if(h!=this.getToolbarHeight()){h=Math.max(0,lsh-this.getFrameHeight());bd.setHeight(h);sz=bd.getSize();this.toolbarHeight=this.getToolbarHeight();this.onBodyResize(sz.width,sz.height);}},onShow:function(){if(this.floating){return this.el.show();} +Ext.Panel.superclass.onShow.call(this);},onHide:function(){if(this.floating){return this.el.hide();} +Ext.Panel.superclass.onHide.call(this);},createToolHandler:function(t,tc,overCls,panel){return function(e){t.removeClass(overCls);if(tc.stopEvent!==false){e.stopEvent();} +if(tc.handler){tc.handler.call(tc.scope||t,e,t,panel,tc);}};},afterRender:function(){if(this.floating&&!this.hidden){this.el.show();} +if(this.title){this.setTitle(this.title);} +Ext.Panel.superclass.afterRender.call(this);if(this.collapsed){this.collapsed=false;this.collapse(false);} +this.initEvents();},getKeyMap:function(){if(!this.keyMap){this.keyMap=new Ext.KeyMap(this.el,this.keys);} +return this.keyMap;},initEvents:function(){if(this.keys){this.getKeyMap();} +if(this.draggable){this.initDraggable();} +if(this.toolbars.length>0){Ext.each(this.toolbars,function(tb){tb.doLayout();tb.on({scope:this,afterlayout:this.syncHeight,remove:this.syncHeight});},this);this.syncHeight();}},initDraggable:function(){this.dd=new Ext.Panel.DD(this,Ext.isBoolean(this.draggable)?null:this.draggable);},beforeEffect:function(anim){if(this.floating){this.el.beforeAction();} +if(anim!==false){this.el.addClass('x-panel-animated');}},afterEffect:function(anim){this.syncShadow();this.el.removeClass('x-panel-animated');},createEffect:function(a,cb,scope){var o={scope:scope,block:true};if(a===true){o.callback=cb;return o;}else if(!a.callback){o.callback=cb;}else{o.callback=function(){cb.call(scope);Ext.callback(a.callback,a.scope);};} +return Ext.applyIf(o,a);},collapse:function(animate){if(this.collapsed||this.el.hasFxBlock()||this.fireEvent('beforecollapse',this,animate)===false){return;} +var doAnim=animate===true||(animate!==false&&this.animCollapse);this.beforeEffect(doAnim);this.onCollapse(doAnim,animate);return this;},onCollapse:function(doAnim,animArg){if(doAnim){this[this.collapseEl].slideOut(this.slideAnchor,Ext.apply(this.createEffect(animArg||true,this.afterCollapse,this),this.collapseDefaults));}else{this[this.collapseEl].hide(this.hideMode);this.afterCollapse(false);}},afterCollapse:function(anim){this.collapsed=true;this.el.addClass(this.collapsedCls);if(anim!==false){this[this.collapseEl].hide(this.hideMode);} +this.afterEffect(anim);this.cascade(function(c){if(c.lastSize){c.lastSize={width:undefined,height:undefined};}});this.fireEvent('collapse',this);},expand:function(animate){if(!this.collapsed||this.el.hasFxBlock()||this.fireEvent('beforeexpand',this,animate)===false){return;} +var doAnim=animate===true||(animate!==false&&this.animCollapse);this.el.removeClass(this.collapsedCls);this.beforeEffect(doAnim);this.onExpand(doAnim,animate);return this;},onExpand:function(doAnim,animArg){if(doAnim){this[this.collapseEl].slideIn(this.slideAnchor,Ext.apply(this.createEffect(animArg||true,this.afterExpand,this),this.expandDefaults));}else{this[this.collapseEl].show(this.hideMode);this.afterExpand(false);}},afterExpand:function(anim){this.collapsed=false;if(anim!==false){this[this.collapseEl].show(this.hideMode);} +this.afterEffect(anim);if(this.deferLayout){delete this.deferLayout;this.doLayout(true);} +this.fireEvent('expand',this);},toggleCollapse:function(animate){this[this.collapsed?'expand':'collapse'](animate);return this;},onDisable:function(){if(this.rendered&&this.maskDisabled){this.el.mask();} +Ext.Panel.superclass.onDisable.call(this);},onEnable:function(){if(this.rendered&&this.maskDisabled){this.el.unmask();} +Ext.Panel.superclass.onEnable.call(this);},onResize:function(adjWidth,adjHeight,rawWidth,rawHeight){var w=adjWidth,h=adjHeight;if(Ext.isDefined(w)||Ext.isDefined(h)){if(!this.collapsed){if(Ext.isNumber(w)){this.body.setWidth(w=this.adjustBodyWidth(w-this.getFrameWidth()));}else if(w=='auto'){w=this.body.setWidth('auto').dom.offsetWidth;}else{w=this.body.dom.offsetWidth;} +if(this.tbar){this.tbar.setWidth(w);if(this.topToolbar){this.topToolbar.setSize(w);}} +if(this.bbar){this.bbar.setWidth(w);if(this.bottomToolbar){this.bottomToolbar.setSize(w);if(Ext.isIE){this.bbar.setStyle('position','static');this.bbar.setStyle('position','');}}} +if(this.footer){this.footer.setWidth(w);if(this.fbar){this.fbar.setSize(Ext.isIE?(w-this.footer.getFrameWidth('lr')):'auto');}} +if(Ext.isNumber(h)){h=Math.max(0,h-this.getFrameHeight());this.body.setHeight(h);}else if(h=='auto'){this.body.setHeight(h);} +if(this.disabled&&this.el._mask){this.el._mask.setSize(this.el.dom.clientWidth,this.el.getHeight());}}else{this.queuedBodySize={width:w,height:h};if(!this.queuedExpand&&this.allowQueuedExpand!==false){this.queuedExpand=true;this.on('expand',function(){delete this.queuedExpand;this.onResize(this.queuedBodySize.width,this.queuedBodySize.height);},this,{single:true});}} +this.onBodyResize(w,h);} +this.syncShadow();Ext.Panel.superclass.onResize.call(this,adjWidth,adjHeight,rawWidth,rawHeight);},onBodyResize:function(w,h){this.fireEvent('bodyresize',this,w,h);},getToolbarHeight:function(){var h=0;if(this.rendered){Ext.each(this.toolbars,function(tb){h+=tb.getHeight();},this);} +return h;},adjustBodyHeight:function(h){return h;},adjustBodyWidth:function(w){return w;},onPosition:function(){this.syncShadow();},getFrameWidth:function(){var w=this.el.getFrameWidth('lr')+this.bwrap.getFrameWidth('lr');if(this.frame){var l=this.bwrap.dom.firstChild;w+=(Ext.fly(l).getFrameWidth('l')+Ext.fly(l.firstChild).getFrameWidth('r'));w+=this.mc.getFrameWidth('lr');} +return w;},getFrameHeight:function(){var h=this.el.getFrameWidth('tb')+this.bwrap.getFrameWidth('tb');h+=(this.tbar?this.tbar.getHeight():0)+ +(this.bbar?this.bbar.getHeight():0);if(this.frame){h+=this.el.dom.firstChild.offsetHeight+this.ft.dom.offsetHeight+this.mc.getFrameWidth('tb');}else{h+=(this.header?this.header.getHeight():0)+ +(this.footer?this.footer.getHeight():0);} +return h;},getInnerWidth:function(){return this.getSize().width-this.getFrameWidth();},getInnerHeight:function(){return this.body.getHeight();},syncShadow:function(){if(this.floating){this.el.sync(true);}},getLayoutTarget:function(){return this.body;},getContentTarget:function(){return this.body;},setTitle:function(title,iconCls){this.title=title;if(this.header&&this.headerAsText){this.header.child('span').update(title);} +if(iconCls){this.setIconClass(iconCls);} +this.fireEvent('titlechange',this,title);return this;},getUpdater:function(){return this.body.getUpdater();},load:function(){var um=this.body.getUpdater();um.update.apply(um,arguments);return this;},beforeDestroy:function(){Ext.Panel.superclass.beforeDestroy.call(this);if(this.header){this.header.removeAllListeners();} +if(this.tools){for(var k in this.tools){Ext.destroy(this.tools[k]);}} +if(this.toolbars.length>0){Ext.each(this.toolbars,function(tb){tb.un('afterlayout',this.syncHeight,this);tb.un('remove',this.syncHeight,this);},this);} +if(Ext.isArray(this.buttons)){while(this.buttons.length){Ext.destroy(this.buttons[0]);}} +if(this.rendered){Ext.destroy(this.ft,this.header,this.footer,this.tbar,this.bbar,this.body,this.mc,this.bwrap,this.dd);if(this.fbar){Ext.destroy(this.fbar,this.fbar.el);}} +Ext.destroy(this.toolbars);},createClasses:function(){this.headerCls=this.baseCls+'-header';this.headerTextCls=this.baseCls+'-header-text';this.bwrapCls=this.baseCls+'-bwrap';this.tbarCls=this.baseCls+'-tbar';this.bodyCls=this.baseCls+'-body';this.bbarCls=this.baseCls+'-bbar';this.footerCls=this.baseCls+'-footer';},createGhost:function(cls,useShim,appendTo){var el=document.createElement('div');el.className='x-panel-ghost '+(cls?cls:'');if(this.header){el.appendChild(this.el.dom.firstChild.cloneNode(true));} +Ext.fly(el.appendChild(document.createElement('ul'))).setHeight(this.bwrap.getHeight());el.style.width=this.el.dom.offsetWidth+'px';;if(!appendTo){this.container.dom.appendChild(el);}else{Ext.getDom(appendTo).appendChild(el);} +if(useShim!==false&&this.el.useShim!==false){var layer=new Ext.Layer({shadow:false,useDisplay:true,constrain:false},el);layer.show();return layer;}else{return new Ext.Element(el);}},doAutoLoad:function(){var u=this.body.getUpdater();if(this.renderer){u.setRenderer(this.renderer);} +u.update(Ext.isObject(this.autoLoad)?this.autoLoad:{url:this.autoLoad});},getTool:function(id){return this.tools[id];}});Ext.reg('panel',Ext.Panel);Ext.Editor=function(field,config){if(field.field){this.field=Ext.create(field.field,'textfield');config=Ext.apply({},field);delete config.field;}else{this.field=field;} +Ext.Editor.superclass.constructor.call(this,config);};Ext.extend(Ext.Editor,Ext.Component,{allowBlur:true,value:"",alignment:"c-c?",offsets:[0,0],shadow:"frame",constrain:false,swallowKeys:true,completeOnEnter:true,cancelOnEsc:true,updateEl:false,initComponent:function(){Ext.Editor.superclass.initComponent.call(this);this.addEvents("beforestartedit","startedit","beforecomplete","complete","canceledit","specialkey");},onRender:function(ct,position){this.el=new Ext.Layer({shadow:this.shadow,cls:"x-editor",parentEl:ct,shim:this.shim,shadowOffset:this.shadowOffset||4,id:this.id,constrain:this.constrain});if(this.zIndex){this.el.setZIndex(this.zIndex);} +this.el.setStyle("overflow",Ext.isGecko?"auto":"hidden");if(this.field.msgTarget!='title'){this.field.msgTarget='qtip';} +this.field.inEditor=true;this.mon(this.field,{scope:this,blur:this.onBlur,specialkey:this.onSpecialKey});if(this.field.grow){this.mon(this.field,"autosize",this.el.sync,this.el,{delay:1});} +this.field.render(this.el).show();this.field.getEl().dom.name='';if(this.swallowKeys){this.field.el.swallowEvent(['keypress','keydown']);}},onSpecialKey:function(field,e){var key=e.getKey(),complete=this.completeOnEnter&&key==e.ENTER,cancel=this.cancelOnEsc&&key==e.ESC;if(complete||cancel){e.stopEvent();if(complete){this.completeEdit();}else{this.cancelEdit();} +if(field.triggerBlur){field.triggerBlur();}} +this.fireEvent('specialkey',field,e);},startEdit:function(el,value){if(this.editing){this.completeEdit();} +this.boundEl=Ext.get(el);var v=value!==undefined?value:this.boundEl.dom.innerHTML;if(!this.rendered){this.render(this.parentEl||document.body);} +if(this.fireEvent("beforestartedit",this,this.boundEl,v)!==false){this.startValue=v;this.field.reset();this.field.setValue(v);this.realign(true);this.editing=true;this.show();}},doAutoSize:function(){if(this.autoSize){var sz=this.boundEl.getSize(),fs=this.field.getSize();switch(this.autoSize){case"width":this.setSize(sz.width,fs.height);break;case"height":this.setSize(fs.width,sz.height);break;case"none":this.setSize(fs.width,fs.height);break;default:this.setSize(sz.width,sz.height);}}},setSize:function(w,h){delete this.field.lastSize;this.field.setSize(w,h);if(this.el){if(Ext.isGecko2||Ext.isOpera||(Ext.isIE7&&Ext.isStrict)){this.el.setSize(w,h);} +this.el.sync();}},realign:function(autoSize){if(autoSize===true){this.doAutoSize();} +this.el.alignTo(this.boundEl,this.alignment,this.offsets);},completeEdit:function(remainVisible){if(!this.editing){return;} +if(this.field.assertValue){this.field.assertValue();} +var v=this.getValue();if(!this.field.isValid()){if(this.revertInvalid!==false){this.cancelEdit(remainVisible);} +return;} +if(String(v)===String(this.startValue)&&this.ignoreNoChange){this.hideEdit(remainVisible);return;} +if(this.fireEvent("beforecomplete",this,v,this.startValue)!==false){v=this.getValue();if(this.updateEl&&this.boundEl){this.boundEl.update(v);} +this.hideEdit(remainVisible);this.fireEvent("complete",this,v,this.startValue);}},onShow:function(){this.el.show();if(this.hideEl!==false){this.boundEl.hide();} +this.field.show().focus(false,true);this.fireEvent("startedit",this.boundEl,this.startValue);},cancelEdit:function(remainVisible){if(this.editing){var v=this.getValue();this.setValue(this.startValue);this.hideEdit(remainVisible);this.fireEvent("canceledit",this,v,this.startValue);}},hideEdit:function(remainVisible){if(remainVisible!==true){this.editing=false;this.hide();}},onBlur:function(){if(this.allowBlur===true&&this.editing&&this.selectSameEditor!==true){this.completeEdit();}},onHide:function(){if(this.editing){this.completeEdit();return;} +this.field.blur();if(this.field.collapse){this.field.collapse();} +this.el.hide();if(this.hideEl!==false){this.boundEl.show();}},setValue:function(v){this.field.setValue(v);},getValue:function(){return this.field.getValue();},beforeDestroy:function(){Ext.destroyMembers(this,'field');delete this.parentEl;delete this.boundEl;}});Ext.reg('editor',Ext.Editor);Ext.ColorPalette=Ext.extend(Ext.Component,{itemCls:'x-color-palette',value:null,clickEvent:'click',ctype:'Ext.ColorPalette',allowReselect:false,colors:['000000','993300','333300','003300','003366','000080','333399','333333','800000','FF6600','808000','008000','008080','0000FF','666699','808080','FF0000','FF9900','99CC00','339966','33CCCC','3366FF','800080','969696','FF00FF','FFCC00','FFFF00','00FF00','00FFFF','00CCFF','993366','C0C0C0','FF99CC','FFCC99','FFFF99','CCFFCC','CCFFFF','99CCFF','CC99FF','FFFFFF'],initComponent:function(){Ext.ColorPalette.superclass.initComponent.call(this);this.addEvents('select');if(this.handler){this.on('select',this.handler,this.scope,true);}},onRender:function(container,position){this.autoEl={tag:'div',cls:this.itemCls};Ext.ColorPalette.superclass.onRender.call(this,container,position);var t=this.tpl||new Ext.XTemplate('<tpl for="."><a href="#" class="color-{.}" hidefocus="on"><em><span style="background:#{.}" unselectable="on"> </span></em></a></tpl>');t.overwrite(this.el,this.colors);this.mon(this.el,this.clickEvent,this.handleClick,this,{delegate:'a'});if(this.clickEvent!='click'){this.mon(this.el,'click',Ext.emptyFn,this,{delegate:'a',preventDefault:true});}},afterRender:function(){Ext.ColorPalette.superclass.afterRender.call(this);if(this.value){var s=this.value;this.value=null;this.select(s,true);}},handleClick:function(e,t){e.preventDefault();if(!this.disabled){var c=t.className.match(/(?:^|\s)color-(.{6})(?:\s|$)/)[1];this.select(c.toUpperCase());}},select:function(color,suppressEvent){color=color.replace('#','');if(color!=this.value||this.allowReselect){var el=this.el;if(this.value){el.child('a.color-'+this.value).removeClass('x-color-palette-sel');} +el.child('a.color-'+color).addClass('x-color-palette-sel');this.value=color;if(suppressEvent!==true){this.fireEvent('select',this,color);}}}});Ext.reg('colorpalette',Ext.ColorPalette);Ext.DatePicker=Ext.extend(Ext.BoxComponent,{todayText:'Today',okText:' OK ',cancelText:'Cancel',todayTip:'{0} (Spacebar)',minText:'This date is before the minimum date',maxText:'This date is after the maximum date',format:'m/d/y',disabledDaysText:'Disabled',disabledDatesText:'Disabled',monthNames:Date.monthNames,dayNames:Date.dayNames,nextText:'Next Month (Control+Right)',prevText:'Previous Month (Control+Left)',monthYearText:'Choose a month (Control+Up/Down to move years)',startDay:0,showToday:true,focusOnSelect:true,initHour:12,initComponent:function(){Ext.DatePicker.superclass.initComponent.call(this);this.value=this.value?this.value.clearTime(true):new Date().clearTime();this.addEvents('select');if(this.handler){this.on('select',this.handler,this.scope||this);} +this.initDisabledDays();},initDisabledDays:function(){if(!this.disabledDatesRE&&this.disabledDates){var dd=this.disabledDates,len=dd.length-1,re='(?:';Ext.each(dd,function(d,i){re+=Ext.isDate(d)?'^'+Ext.escapeRe(d.dateFormat(this.format))+'$':dd[i];if(i!=len){re+='|';}},this);this.disabledDatesRE=new RegExp(re+')');}},setDisabledDates:function(dd){if(Ext.isArray(dd)){this.disabledDates=dd;this.disabledDatesRE=null;}else{this.disabledDatesRE=dd;} +this.initDisabledDays();this.update(this.value,true);},setDisabledDays:function(dd){this.disabledDays=dd;this.update(this.value,true);},setMinDate:function(dt){this.minDate=dt;this.update(this.value,true);},setMaxDate:function(dt){this.maxDate=dt;this.update(this.value,true);},setValue:function(value){this.value=value.clearTime(true);this.update(this.value);},getValue:function(){return this.value;},focus:function(){this.update(this.activeDate);},onEnable:function(initial){Ext.DatePicker.superclass.onEnable.call(this);this.doDisabled(false);this.update(initial?this.value:this.activeDate);if(Ext.isIE){this.el.repaint();}},onDisable:function(){Ext.DatePicker.superclass.onDisable.call(this);this.doDisabled(true);if(Ext.isIE&&!Ext.isIE8){Ext.each([].concat(this.textNodes,this.el.query('th span')),function(el){Ext.fly(el).repaint();});}},doDisabled:function(disabled){this.keyNav.setDisabled(disabled);this.prevRepeater.setDisabled(disabled);this.nextRepeater.setDisabled(disabled);if(this.showToday){this.todayKeyListener.setDisabled(disabled);this.todayBtn.setDisabled(disabled);}},onRender:function(container,position){var m=['<table cellspacing="0">','<tr><td class="x-date-left"><a href="#" title="',this.prevText,'"> </a></td><td class="x-date-middle" align="center"></td><td class="x-date-right"><a href="#" title="',this.nextText,'"> </a></td></tr>','<tr><td colspan="3"><table class="x-date-inner" cellspacing="0"><thead><tr>'],dn=this.dayNames,i;for(i=0;i<7;i++){var d=this.startDay+i;if(d>6){d=d-7;} +m.push('<th><span>',dn[d].substr(0,1),'</span></th>');} +m[m.length]='</tr></thead><tbody><tr>';for(i=0;i<42;i++){if(i%7===0&&i!==0){m[m.length]='</tr><tr>';} +m[m.length]='<td><a href="#" hidefocus="on" class="x-date-date" tabIndex="1"><em><span></span></em></a></td>';} +m.push('</tr></tbody></table></td></tr>',this.showToday?'<tr><td colspan="3" class="x-date-bottom" align="center"></td></tr>':'','</table><div class="x-date-mp"></div>');var el=document.createElement('div');el.className='x-date-picker';el.innerHTML=m.join('');container.dom.insertBefore(el,position);this.el=Ext.get(el);this.eventEl=Ext.get(el.firstChild);this.prevRepeater=new Ext.util.ClickRepeater(this.el.child('td.x-date-left a'),{handler:this.showPrevMonth,scope:this,preventDefault:true,stopDefault:true});this.nextRepeater=new Ext.util.ClickRepeater(this.el.child('td.x-date-right a'),{handler:this.showNextMonth,scope:this,preventDefault:true,stopDefault:true});this.monthPicker=this.el.down('div.x-date-mp');this.monthPicker.enableDisplayMode('block');this.keyNav=new Ext.KeyNav(this.eventEl,{'left':function(e){if(e.ctrlKey){this.showPrevMonth();}else{this.update(this.activeDate.add('d',-1));}},'right':function(e){if(e.ctrlKey){this.showNextMonth();}else{this.update(this.activeDate.add('d',1));}},'up':function(e){if(e.ctrlKey){this.showNextYear();}else{this.update(this.activeDate.add('d',-7));}},'down':function(e){if(e.ctrlKey){this.showPrevYear();}else{this.update(this.activeDate.add('d',7));}},'pageUp':function(e){this.showNextMonth();},'pageDown':function(e){this.showPrevMonth();},'enter':function(e){e.stopPropagation();return true;},scope:this});this.el.unselectable();this.cells=this.el.select('table.x-date-inner tbody td');this.textNodes=this.el.query('table.x-date-inner tbody span');this.mbtn=new Ext.Button({text:' ',tooltip:this.monthYearText,renderTo:this.el.child('td.x-date-middle',true)});this.mbtn.el.child('em').addClass('x-btn-arrow');if(this.showToday){this.todayKeyListener=this.eventEl.addKeyListener(Ext.EventObject.SPACE,this.selectToday,this);var today=(new Date()).dateFormat(this.format);this.todayBtn=new Ext.Button({renderTo:this.el.child('td.x-date-bottom',true),text:String.format(this.todayText,today),tooltip:String.format(this.todayTip,today),handler:this.selectToday,scope:this});} +this.mon(this.eventEl,'mousewheel',this.handleMouseWheel,this);this.mon(this.eventEl,'click',this.handleDateClick,this,{delegate:'a.x-date-date'});this.mon(this.mbtn,'click',this.showMonthPicker,this);this.onEnable(true);},createMonthPicker:function(){if(!this.monthPicker.dom.firstChild){var buf=['<table border="0" cellspacing="0">'];for(var i=0;i<6;i++){buf.push('<tr><td class="x-date-mp-month"><a href="#">',Date.getShortMonthName(i),'</a></td>','<td class="x-date-mp-month x-date-mp-sep"><a href="#">',Date.getShortMonthName(i+6),'</a></td>',i===0?'<td class="x-date-mp-ybtn" align="center"><a class="x-date-mp-prev"></a></td><td class="x-date-mp-ybtn" align="center"><a class="x-date-mp-next"></a></td></tr>':'<td class="x-date-mp-year"><a href="#"></a></td><td class="x-date-mp-year"><a href="#"></a></td></tr>');} +buf.push('<tr class="x-date-mp-btns"><td colspan="4"><button type="button" class="x-date-mp-ok">',this.okText,'</button><button type="button" class="x-date-mp-cancel">',this.cancelText,'</button></td></tr>','</table>');this.monthPicker.update(buf.join(''));this.mon(this.monthPicker,'click',this.onMonthClick,this);this.mon(this.monthPicker,'dblclick',this.onMonthDblClick,this);this.mpMonths=this.monthPicker.select('td.x-date-mp-month');this.mpYears=this.monthPicker.select('td.x-date-mp-year');this.mpMonths.each(function(m,a,i){i+=1;if((i%2)===0){m.dom.xmonth=5+Math.round(i*0.5);}else{m.dom.xmonth=Math.round((i-1)*0.5);}});}},showMonthPicker:function(){if(!this.disabled){this.createMonthPicker();var size=this.el.getSize();this.monthPicker.setSize(size);this.monthPicker.child('table').setSize(size);this.mpSelMonth=(this.activeDate||this.value).getMonth();this.updateMPMonth(this.mpSelMonth);this.mpSelYear=(this.activeDate||this.value).getFullYear();this.updateMPYear(this.mpSelYear);this.monthPicker.slideIn('t',{duration:0.2});}},updateMPYear:function(y){this.mpyear=y;var ys=this.mpYears.elements;for(var i=1;i<=10;i++){var td=ys[i-1],y2;if((i%2)===0){y2=y+Math.round(i*0.5);td.firstChild.innerHTML=y2;td.xyear=y2;}else{y2=y-(5-Math.round(i*0.5));td.firstChild.innerHTML=y2;td.xyear=y2;} +this.mpYears.item(i-1)[y2==this.mpSelYear?'addClass':'removeClass']('x-date-mp-sel');}},updateMPMonth:function(sm){this.mpMonths.each(function(m,a,i){m[m.dom.xmonth==sm?'addClass':'removeClass']('x-date-mp-sel');});},selectMPMonth:function(m){},onMonthClick:function(e,t){e.stopEvent();var el=new Ext.Element(t),pn;if(el.is('button.x-date-mp-cancel')){this.hideMonthPicker();} +else if(el.is('button.x-date-mp-ok')){var d=new Date(this.mpSelYear,this.mpSelMonth,(this.activeDate||this.value).getDate());if(d.getMonth()!=this.mpSelMonth){d=new Date(this.mpSelYear,this.mpSelMonth,1).getLastDateOfMonth();} +this.update(d);this.hideMonthPicker();} +else if((pn=el.up('td.x-date-mp-month',2))){this.mpMonths.removeClass('x-date-mp-sel');pn.addClass('x-date-mp-sel');this.mpSelMonth=pn.dom.xmonth;} +else if((pn=el.up('td.x-date-mp-year',2))){this.mpYears.removeClass('x-date-mp-sel');pn.addClass('x-date-mp-sel');this.mpSelYear=pn.dom.xyear;} +else if(el.is('a.x-date-mp-prev')){this.updateMPYear(this.mpyear-10);} +else if(el.is('a.x-date-mp-next')){this.updateMPYear(this.mpyear+10);}},onMonthDblClick:function(e,t){e.stopEvent();var el=new Ext.Element(t),pn;if((pn=el.up('td.x-date-mp-month',2))){this.update(new Date(this.mpSelYear,pn.dom.xmonth,(this.activeDate||this.value).getDate()));this.hideMonthPicker();} +else if((pn=el.up('td.x-date-mp-year',2))){this.update(new Date(pn.dom.xyear,this.mpSelMonth,(this.activeDate||this.value).getDate()));this.hideMonthPicker();}},hideMonthPicker:function(disableAnim){if(this.monthPicker){if(disableAnim===true){this.monthPicker.hide();}else{this.monthPicker.slideOut('t',{duration:0.2});}}},showPrevMonth:function(e){this.update(this.activeDate.add('mo',-1));},showNextMonth:function(e){this.update(this.activeDate.add('mo',1));},showPrevYear:function(){this.update(this.activeDate.add('y',-1));},showNextYear:function(){this.update(this.activeDate.add('y',1));},handleMouseWheel:function(e){e.stopEvent();if(!this.disabled){var delta=e.getWheelDelta();if(delta>0){this.showPrevMonth();}else if(delta<0){this.showNextMonth();}}},handleDateClick:function(e,t){e.stopEvent();if(!this.disabled&&t.dateValue&&!Ext.fly(t.parentNode).hasClass('x-date-disabled')){this.cancelFocus=this.focusOnSelect===false;this.setValue(new Date(t.dateValue));delete this.cancelFocus;this.fireEvent('select',this,this.value);}},selectToday:function(){if(this.todayBtn&&!this.todayBtn.disabled){this.setValue(new Date().clearTime());this.fireEvent('select',this,this.value);}},update:function(date,forceRefresh){if(this.rendered){var vd=this.activeDate,vis=this.isVisible();this.activeDate=date;if(!forceRefresh&&vd&&this.el){var t=date.getTime();if(vd.getMonth()==date.getMonth()&&vd.getFullYear()==date.getFullYear()){this.cells.removeClass('x-date-selected');this.cells.each(function(c){if(c.dom.firstChild.dateValue==t){c.addClass('x-date-selected');if(vis&&!this.cancelFocus){Ext.fly(c.dom.firstChild).focus(50);} +return false;}},this);return;}} +var days=date.getDaysInMonth(),firstOfMonth=date.getFirstDateOfMonth(),startingPos=firstOfMonth.getDay()-this.startDay;if(startingPos<0){startingPos+=7;} +days+=startingPos;var pm=date.add('mo',-1),prevStart=pm.getDaysInMonth()-startingPos,cells=this.cells.elements,textEls=this.textNodes,d=(new Date(pm.getFullYear(),pm.getMonth(),prevStart,this.initHour)),today=new Date().clearTime().getTime(),sel=date.clearTime(true).getTime(),min=this.minDate?this.minDate.clearTime(true):Number.NEGATIVE_INFINITY,max=this.maxDate?this.maxDate.clearTime(true):Number.POSITIVE_INFINITY,ddMatch=this.disabledDatesRE,ddText=this.disabledDatesText,ddays=this.disabledDays?this.disabledDays.join(''):false,ddaysText=this.disabledDaysText,format=this.format;if(this.showToday){var td=new Date().clearTime(),disable=(td<min||td>max||(ddMatch&&format&&ddMatch.test(td.dateFormat(format)))||(ddays&&ddays.indexOf(td.getDay())!=-1));if(!this.disabled){this.todayBtn.setDisabled(disable);this.todayKeyListener[disable?'disable':'enable']();}} +var setCellClass=function(cal,cell){cell.title='';var t=d.clearTime(true).getTime();cell.firstChild.dateValue=t;if(t==today){cell.className+=' x-date-today';cell.title=cal.todayText;} +if(t==sel){cell.className+=' x-date-selected';if(vis){Ext.fly(cell.firstChild).focus(50);}} +if(t<min){cell.className=' x-date-disabled';cell.title=cal.minText;return;} +if(t>max){cell.className=' x-date-disabled';cell.title=cal.maxText;return;} +if(ddays){if(ddays.indexOf(d.getDay())!=-1){cell.title=ddaysText;cell.className=' x-date-disabled';}} +if(ddMatch&&format){var fvalue=d.dateFormat(format);if(ddMatch.test(fvalue)){cell.title=ddText.replace('%0',fvalue);cell.className=' x-date-disabled';}}};var i=0;for(;i<startingPos;i++){textEls[i].innerHTML=(++prevStart);d.setDate(d.getDate()+1);cells[i].className='x-date-prevday';setCellClass(this,cells[i]);} +for(;i<days;i++){var intDay=i-startingPos+1;textEls[i].innerHTML=(intDay);d.setDate(d.getDate()+1);cells[i].className='x-date-active';setCellClass(this,cells[i]);} +var extraDays=0;for(;i<42;i++){textEls[i].innerHTML=(++extraDays);d.setDate(d.getDate()+1);cells[i].className='x-date-nextday';setCellClass(this,cells[i]);} +this.mbtn.setText(this.monthNames[date.getMonth()]+' '+date.getFullYear());if(!this.internalRender){var main=this.el.dom.firstChild,w=main.offsetWidth;this.el.setWidth(w+this.el.getBorderWidth('lr'));Ext.fly(main).setWidth(w);this.internalRender=true;if(Ext.isOpera&&!this.secondPass){main.rows[0].cells[1].style.width=(w-(main.rows[0].cells[0].offsetWidth+main.rows[0].cells[2].offsetWidth))+'px';this.secondPass=true;this.update.defer(10,this,[date]);}}}},beforeDestroy:function(){if(this.rendered){Ext.destroy(this.keyNav,this.monthPicker,this.eventEl,this.mbtn,this.nextRepeater,this.prevRepeater,this.cells.el,this.todayBtn);delete this.textNodes;delete this.cells.elements;}}});Ext.reg('datepicker',Ext.DatePicker);Ext.LoadMask=function(el,config){this.el=Ext.get(el);Ext.apply(this,config);if(this.store){this.store.on({scope:this,beforeload:this.onBeforeLoad,load:this.onLoad,exception:this.onLoad});this.removeMask=Ext.value(this.removeMask,false);}else{var um=this.el.getUpdater();um.showLoadIndicator=false;um.on({scope:this,beforeupdate:this.onBeforeLoad,update:this.onLoad,failure:this.onLoad});this.removeMask=Ext.value(this.removeMask,true);}};Ext.LoadMask.prototype={msg:'Loading...',msgCls:'x-mask-loading',disabled:false,disable:function(){this.disabled=true;},enable:function(){this.disabled=false;},onLoad:function(){this.el.unmask(this.removeMask);},onBeforeLoad:function(){if(!this.disabled){this.el.mask(this.msg,this.msgCls);}},show:function(){this.onBeforeLoad();},hide:function(){this.onLoad();},destroy:function(){if(this.store){this.store.un('beforeload',this.onBeforeLoad,this);this.store.un('load',this.onLoad,this);this.store.un('exception',this.onLoad,this);}else{var um=this.el.getUpdater();um.un('beforeupdate',this.onBeforeLoad,this);um.un('update',this.onLoad,this);um.un('failure',this.onLoad,this);}}};Ext.ns('Ext.slider');Ext.slider.Thumb=Ext.extend(Object,{dragging:false,constructor:function(config){Ext.apply(this,config||{},{cls:'x-slider-thumb',constrain:false});Ext.slider.Thumb.superclass.constructor.call(this,config);if(this.slider.vertical){Ext.apply(this,Ext.slider.Thumb.Vertical);}},render:function(){this.el=this.slider.innerEl.insertFirst({cls:this.cls});this.initEvents();},enable:function(){this.disabled=false;this.el.removeClass(this.slider.disabledClass);},disable:function(){this.disabled=true;this.el.addClass(this.slider.disabledClass);},initEvents:function(){var el=this.el;el.addClassOnOver('x-slider-thumb-over');this.tracker=new Ext.dd.DragTracker({onBeforeStart:this.onBeforeDragStart.createDelegate(this),onStart:this.onDragStart.createDelegate(this),onDrag:this.onDrag.createDelegate(this),onEnd:this.onDragEnd.createDelegate(this),tolerance:3,autoStart:300});this.tracker.initEl(el);},onBeforeDragStart:function(e){if(this.disabled){return false;}else{this.slider.promoteThumb(this);return true;}},onDragStart:function(e){this.el.addClass('x-slider-thumb-drag');this.dragging=true;this.dragStartValue=this.value;this.slider.fireEvent('dragstart',this.slider,e,this);},onDrag:function(e){var slider=this.slider,index=this.index,newValue=this.getNewValue();if(this.constrain){var above=slider.thumbs[index+1],below=slider.thumbs[index-1];if(below!=undefined&&newValue<=below.value)newValue=below.value;if(above!=undefined&&newValue>=above.value)newValue=above.value;} +slider.setValue(index,newValue,false);slider.fireEvent('drag',slider,e,this);},getNewValue:function(){var slider=this.slider,pos=slider.innerEl.translatePoints(this.tracker.getXY());return Ext.util.Format.round(slider.reverseValue(pos.left),slider.decimalPrecision);},onDragEnd:function(e){var slider=this.slider,value=this.value;this.el.removeClass('x-slider-thumb-drag');this.dragging=false;slider.fireEvent('dragend',slider,e);if(this.dragStartValue!=value){slider.fireEvent('changecomplete',slider,value,this);}},destroy:function(){Ext.destroyMembers(this,'tracker','el');}});Ext.slider.MultiSlider=Ext.extend(Ext.BoxComponent,{vertical:false,minValue:0,maxValue:100,decimalPrecision:0,keyIncrement:1,increment:0,clickRange:[5,15],clickToChange:true,animate:true,constrainThumbs:true,topThumbZIndex:10000,initComponent:function(){if(!Ext.isDefined(this.value)){this.value=this.minValue;} +this.thumbs=[];Ext.slider.MultiSlider.superclass.initComponent.call(this);this.keyIncrement=Math.max(this.increment,this.keyIncrement);this.addEvents('beforechange','change','changecomplete','dragstart','drag','dragend');if(this.values==undefined||Ext.isEmpty(this.values))this.values=[0];var values=this.values;for(var i=0;i<values.length;i++){this.addThumb(values[i]);} +if(this.vertical){Ext.apply(this,Ext.slider.Vertical);}},addThumb:function(value){var thumb=new Ext.slider.Thumb({value:value,slider:this,index:this.thumbs.length,constrain:this.constrainThumbs});this.thumbs.push(thumb);if(this.rendered)thumb.render();},promoteThumb:function(topThumb){var thumbs=this.thumbs,zIndex,thumb;for(var i=0,j=thumbs.length;i<j;i++){thumb=thumbs[i];if(thumb==topThumb){zIndex=this.topThumbZIndex;}else{zIndex='';} +thumb.el.setStyle('zIndex',zIndex);}},onRender:function(){this.autoEl={cls:'x-slider '+(this.vertical?'x-slider-vert':'x-slider-horz'),cn:{cls:'x-slider-end',cn:{cls:'x-slider-inner',cn:[{tag:'a',cls:'x-slider-focus',href:"#",tabIndex:'-1',hidefocus:'on'}]}}};Ext.slider.MultiSlider.superclass.onRender.apply(this,arguments);this.endEl=this.el.first();this.innerEl=this.endEl.first();this.focusEl=this.innerEl.child('.x-slider-focus');for(var i=0;i<this.thumbs.length;i++){this.thumbs[i].render();} +var thumb=this.innerEl.child('.x-slider-thumb');this.halfThumb=(this.vertical?thumb.getHeight():thumb.getWidth())/2;this.initEvents();},initEvents:function(){this.mon(this.el,{scope:this,mousedown:this.onMouseDown,keydown:this.onKeyDown});this.focusEl.swallowEvent("click",true);},onMouseDown:function(e){if(this.disabled){return;} +var thumbClicked=false;for(var i=0;i<this.thumbs.length;i++){thumbClicked=thumbClicked||e.target==this.thumbs[i].el.dom;} +if(this.clickToChange&&!thumbClicked){var local=this.innerEl.translatePoints(e.getXY());this.onClickChange(local);} +this.focus();},onClickChange:function(local){if(local.top>this.clickRange[0]&&local.top<this.clickRange[1]){var thumb=this.getNearest(local,'left'),index=thumb.index;this.setValue(index,Ext.util.Format.round(this.reverseValue(local.left),this.decimalPrecision),undefined,true);}},getNearest:function(local,prop){var localValue=prop=='top'?this.innerEl.getHeight()-local[prop]:local[prop],clickValue=this.reverseValue(localValue),nearestDistance=(this.maxValue-this.minValue)+5,index=0,nearest=null;for(var i=0;i<this.thumbs.length;i++){var thumb=this.thumbs[i],value=thumb.value,dist=Math.abs(value-clickValue);if(Math.abs(dist<=nearestDistance)){nearest=thumb;index=i;nearestDistance=dist;}} +return nearest;},onKeyDown:function(e){if(this.disabled||this.thumbs.length!==1){e.preventDefault();return;} +var k=e.getKey(),val;switch(k){case e.UP:case e.RIGHT:e.stopEvent();val=e.ctrlKey?this.maxValue:this.getValue(0)+this.keyIncrement;this.setValue(0,val,undefined,true);break;case e.DOWN:case e.LEFT:e.stopEvent();val=e.ctrlKey?this.minValue:this.getValue(0)-this.keyIncrement;this.setValue(0,val,undefined,true);break;default:e.preventDefault();}},doSnap:function(value){if(!(this.increment&&value)){return value;} +var newValue=value,inc=this.increment,m=value%inc;if(m!=0){newValue-=m;if(m*2>=inc){newValue+=inc;}else if(m*2<-inc){newValue-=inc;}} +return newValue.constrain(this.minValue,this.maxValue);},afterRender:function(){Ext.slider.MultiSlider.superclass.afterRender.apply(this,arguments);for(var i=0;i<this.thumbs.length;i++){var thumb=this.thumbs[i];if(thumb.value!==undefined){var v=this.normalizeValue(thumb.value);if(v!==thumb.value){this.setValue(i,v,false);}else{this.moveThumb(i,this.translateValue(v),false);}}};},getRatio:function(){var w=this.innerEl.getWidth(),v=this.maxValue-this.minValue;return v==0?w:(w/v);},normalizeValue:function(v){v=this.doSnap(v);v=Ext.util.Format.round(v,this.decimalPrecision);v=v.constrain(this.minValue,this.maxValue);return v;},setMinValue:function(val){this.minValue=val;var i=0,thumbs=this.thumbs,len=thumbs.length,t;for(;i<len;++i){t=thumbs[i];t.value=t.value<val?val:t.value;} +this.syncThumb();},setMaxValue:function(val){this.maxValue=val;var i=0,thumbs=this.thumbs,len=thumbs.length,t;for(;i<len;++i){t=thumbs[i];t.value=t.value>val?val:t.value;} +this.syncThumb();},setValue:function(index,v,animate,changeComplete){var thumb=this.thumbs[index],el=thumb.el;v=this.normalizeValue(v);if(v!==thumb.value&&this.fireEvent('beforechange',this,v,thumb.value,thumb)!==false){thumb.value=v;if(this.rendered){this.moveThumb(index,this.translateValue(v),animate!==false);this.fireEvent('change',this,v,thumb);if(changeComplete){this.fireEvent('changecomplete',this,v,thumb);}}}},translateValue:function(v){var ratio=this.getRatio();return(v*ratio)-(this.minValue*ratio)-this.halfThumb;},reverseValue:function(pos){var ratio=this.getRatio();return(pos+(this.minValue*ratio))/ratio;},moveThumb:function(index,v,animate){var thumb=this.thumbs[index].el;if(!animate||this.animate===false){thumb.setLeft(v);}else{thumb.shift({left:v,stopFx:true,duration:.35});}},focus:function(){this.focusEl.focus(10);},onResize:function(w,h){var thumbs=this.thumbs,len=thumbs.length,i=0;for(;i<len;++i){thumbs[i].el.stopFx();} +if(Ext.isNumber(w)){this.innerEl.setWidth(w-(this.el.getPadding('l')+this.endEl.getPadding('r')));} +this.syncThumb();Ext.slider.MultiSlider.superclass.onResize.apply(this,arguments);},onDisable:function(){Ext.slider.MultiSlider.superclass.onDisable.call(this);for(var i=0;i<this.thumbs.length;i++){var thumb=this.thumbs[i],el=thumb.el;thumb.disable();if(Ext.isIE){var xy=el.getXY();el.hide();this.innerEl.addClass(this.disabledClass).dom.disabled=true;if(!this.thumbHolder){this.thumbHolder=this.endEl.createChild({cls:'x-slider-thumb '+this.disabledClass});} +this.thumbHolder.show().setXY(xy);}}},onEnable:function(){Ext.slider.MultiSlider.superclass.onEnable.call(this);for(var i=0;i<this.thumbs.length;i++){var thumb=this.thumbs[i],el=thumb.el;thumb.enable();if(Ext.isIE){this.innerEl.removeClass(this.disabledClass).dom.disabled=false;if(this.thumbHolder)this.thumbHolder.hide();el.show();this.syncThumb();}}},syncThumb:function(){if(this.rendered){for(var i=0;i<this.thumbs.length;i++){this.moveThumb(i,this.translateValue(this.thumbs[i].value));}}},getValue:function(index){return this.thumbs[index].value;},getValues:function(){var values=[];for(var i=0;i<this.thumbs.length;i++){values.push(this.thumbs[i].value);} +return values;},beforeDestroy:function(){var thumbs=this.thumbs;for(var i=0,len=thumbs.length;i<len;++i){thumbs[i].destroy();thumbs[i]=null;} +Ext.destroyMembers(this,'endEl','innerEl','focusEl','thumbHolder');Ext.slider.MultiSlider.superclass.beforeDestroy.call(this);}});Ext.reg('multislider',Ext.slider.MultiSlider);Ext.slider.SingleSlider=Ext.extend(Ext.slider.MultiSlider,{constructor:function(config){config=config||{};Ext.applyIf(config,{values:[config.value||0]});Ext.slider.SingleSlider.superclass.constructor.call(this,config);},getValue:function(){return Ext.slider.SingleSlider.superclass.getValue.call(this,0);},setValue:function(value,animate){var args=Ext.toArray(arguments),len=args.length;if(len==1||(len<=3&&typeof arguments[1]!='number')){args.unshift(0);} +return Ext.slider.SingleSlider.superclass.setValue.apply(this,args);},syncThumb:function(){return Ext.slider.SingleSlider.superclass.syncThumb.apply(this,[0].concat(arguments));},getNearest:function(){return this.thumbs[0];}});Ext.Slider=Ext.slider.SingleSlider;Ext.reg('slider',Ext.slider.SingleSlider);Ext.slider.Vertical={onResize:function(w,h){this.innerEl.setHeight(h-(this.el.getPadding('t')+this.endEl.getPadding('b')));this.syncThumb();},getRatio:function(){var h=this.innerEl.getHeight(),v=this.maxValue-this.minValue;return h/v;},moveThumb:function(index,v,animate){var thumb=this.thumbs[index],el=thumb.el;if(!animate||this.animate===false){el.setBottom(v);}else{el.shift({bottom:v,stopFx:true,duration:.35});}},onClickChange:function(local){if(local.left>this.clickRange[0]&&local.left<this.clickRange[1]){var thumb=this.getNearest(local,'top'),index=thumb.index,value=this.minValue+this.reverseValue(this.innerEl.getHeight()-local.top);this.setValue(index,Ext.util.Format.round(value,this.decimalPrecision),undefined,true);}}};Ext.slider.Thumb.Vertical={getNewValue:function(){var slider=this.slider,innerEl=slider.innerEl,pos=innerEl.translatePoints(this.tracker.getXY()),bottom=innerEl.getHeight()-pos.top;return slider.minValue+Ext.util.Format.round(bottom/slider.getRatio(),slider.decimalPrecision);}};Ext.ProgressBar=Ext.extend(Ext.BoxComponent,{baseCls:'x-progress',animate:false,waitTimer:null,initComponent:function(){Ext.ProgressBar.superclass.initComponent.call(this);this.addEvents("update");},onRender:function(ct,position){var tpl=new Ext.Template('<div class="{cls}-wrap">','<div class="{cls}-inner">','<div class="{cls}-bar">','<div class="{cls}-text">','<div> </div>','</div>','</div>','<div class="{cls}-text {cls}-text-back">','<div> </div>','</div>','</div>','</div>');this.el=position?tpl.insertBefore(position,{cls:this.baseCls},true):tpl.append(ct,{cls:this.baseCls},true);if(this.id){this.el.dom.id=this.id;} +var inner=this.el.dom.firstChild;this.progressBar=Ext.get(inner.firstChild);if(this.textEl){this.textEl=Ext.get(this.textEl);delete this.textTopEl;}else{this.textTopEl=Ext.get(this.progressBar.dom.firstChild);var textBackEl=Ext.get(inner.childNodes[1]);this.textTopEl.setStyle("z-index",99).addClass('x-hidden');this.textEl=new Ext.CompositeElement([this.textTopEl.dom.firstChild,textBackEl.dom.firstChild]);this.textEl.setWidth(inner.offsetWidth);} +this.progressBar.setHeight(inner.offsetHeight);},afterRender:function(){Ext.ProgressBar.superclass.afterRender.call(this);if(this.value){this.updateProgress(this.value,this.text);}else{this.updateText(this.text);}},updateProgress:function(value,text,animate){this.value=value||0;if(text){this.updateText(text);} +if(this.rendered&&!this.isDestroyed){var w=Math.floor(value*this.el.dom.firstChild.offsetWidth);this.progressBar.setWidth(w,animate===true||(animate!==false&&this.animate));if(this.textTopEl){this.textTopEl.removeClass('x-hidden').setWidth(w);}} +this.fireEvent('update',this,value,text);return this;},wait:function(o){if(!this.waitTimer){var scope=this;o=o||{};this.updateText(o.text);this.waitTimer=Ext.TaskMgr.start({run:function(i){var inc=o.increment||10;i-=1;this.updateProgress(((((i+inc)%inc)+1)*(100/inc))*0.01,null,o.animate);},interval:o.interval||1000,duration:o.duration,onStop:function(){if(o.fn){o.fn.apply(o.scope||this);} +this.reset();},scope:scope});} +return this;},isWaiting:function(){return this.waitTimer!==null;},updateText:function(text){this.text=text||' ';if(this.rendered){this.textEl.update(this.text);} +return this;},syncProgressBar:function(){if(this.value){this.updateProgress(this.value,this.text);} +return this;},setSize:function(w,h){Ext.ProgressBar.superclass.setSize.call(this,w,h);if(this.textTopEl){var inner=this.el.dom.firstChild;this.textEl.setSize(inner.offsetWidth,inner.offsetHeight);} +this.syncProgressBar();return this;},reset:function(hide){this.updateProgress(0);if(this.textTopEl){this.textTopEl.addClass('x-hidden');} +this.clearTimer();if(hide===true){this.hide();} +return this;},clearTimer:function(){if(this.waitTimer){this.waitTimer.onStop=null;Ext.TaskMgr.stop(this.waitTimer);this.waitTimer=null;}},onDestroy:function(){this.clearTimer();if(this.rendered){if(this.textEl.isComposite){this.textEl.clear();} +Ext.destroyMembers(this,'textEl','progressBar','textTopEl');} +Ext.ProgressBar.superclass.onDestroy.call(this);}});Ext.reg('progress',Ext.ProgressBar);Ext.DataView=Ext.extend(Ext.BoxComponent,{selectedClass:"x-view-selected",emptyText:"",deferEmptyText:true,trackOver:false,blockRefresh:false,last:false,initComponent:function(){Ext.DataView.superclass.initComponent.call(this);if(Ext.isString(this.tpl)||Ext.isArray(this.tpl)){this.tpl=new Ext.XTemplate(this.tpl);} +this.addEvents("beforeclick","click","mouseenter","mouseleave","containerclick","dblclick","contextmenu","containercontextmenu","selectionchange","beforeselect");this.store=Ext.StoreMgr.lookup(this.store);this.all=new Ext.CompositeElementLite();this.selected=new Ext.CompositeElementLite();},afterRender:function(){Ext.DataView.superclass.afterRender.call(this);this.mon(this.getTemplateTarget(),{"click":this.onClick,"dblclick":this.onDblClick,"contextmenu":this.onContextMenu,scope:this});if(this.overClass||this.trackOver){this.mon(this.getTemplateTarget(),{"mouseover":this.onMouseOver,"mouseout":this.onMouseOut,scope:this});} +if(this.store){this.bindStore(this.store,true);}},refresh:function(){this.clearSelections(false,true);var el=this.getTemplateTarget(),records=this.store.getRange();el.update('');if(records.length<1){if(!this.deferEmptyText||this.hasSkippedEmptyText){el.update(this.emptyText);} +this.all.clear();}else{this.tpl.overwrite(el,this.collectData(records,0));this.all.fill(Ext.query(this.itemSelector,el.dom));this.updateIndexes(0);} +this.hasSkippedEmptyText=true;},getTemplateTarget:function(){return this.el;},prepareData:function(data){return data;},collectData:function(records,startIndex){var r=[],i=0,len=records.length;for(;i<len;i++){r[r.length]=this.prepareData(records[i].data,startIndex+i,records[i]);} +return r;},bufferRender:function(records,index){var div=document.createElement('div');this.tpl.overwrite(div,this.collectData(records,index));return Ext.query(this.itemSelector,div);},onUpdate:function(ds,record){var index=this.store.indexOf(record);if(index>-1){var sel=this.isSelected(index),original=this.all.elements[index],node=this.bufferRender([record],index)[0];this.all.replaceElement(index,node,true);if(sel){this.selected.replaceElement(original,node);this.all.item(index).addClass(this.selectedClass);} +this.updateIndexes(index,index);}},onAdd:function(ds,records,index){if(this.all.getCount()===0){this.refresh();return;} +var nodes=this.bufferRender(records,index),n,a=this.all.elements;if(index<this.all.getCount()){n=this.all.item(index).insertSibling(nodes,'before',true);a.splice.apply(a,[index,0].concat(nodes));}else{n=this.all.last().insertSibling(nodes,'after',true);a.push.apply(a,nodes);} +this.updateIndexes(index);},onRemove:function(ds,record,index){this.deselect(index);this.all.removeElement(index,true);this.updateIndexes(index);if(this.store.getCount()===0){this.refresh();}},refreshNode:function(index){this.onUpdate(this.store,this.store.getAt(index));},updateIndexes:function(startIndex,endIndex){var ns=this.all.elements;startIndex=startIndex||0;endIndex=endIndex||((endIndex===0)?0:(ns.length-1));for(var i=startIndex;i<=endIndex;i++){ns[i].viewIndex=i;}},getStore:function(){return this.store;},bindStore:function(store,initial){if(!initial&&this.store){if(store!==this.store&&this.store.autoDestroy){this.store.destroy();}else{this.store.un("beforeload",this.onBeforeLoad,this);this.store.un("datachanged",this.onDataChanged,this);this.store.un("add",this.onAdd,this);this.store.un("remove",this.onRemove,this);this.store.un("update",this.onUpdate,this);this.store.un("clear",this.refresh,this);} +if(!store){this.store=null;}} +if(store){store=Ext.StoreMgr.lookup(store);store.on({scope:this,beforeload:this.onBeforeLoad,datachanged:this.onDataChanged,add:this.onAdd,remove:this.onRemove,update:this.onUpdate,clear:this.refresh});} +this.store=store;if(store){this.refresh();}},onDataChanged:function(){if(this.blockRefresh!==true){this.refresh.apply(this,arguments);}},findItemFromChild:function(node){return Ext.fly(node).findParent(this.itemSelector,this.getTemplateTarget());},onClick:function(e){var item=e.getTarget(this.itemSelector,this.getTemplateTarget()),index;if(item){index=this.indexOf(item);if(this.onItemClick(item,index,e)!==false){this.fireEvent("click",this,index,item,e);}}else{if(this.fireEvent("containerclick",this,e)!==false){this.onContainerClick(e);}}},onContainerClick:function(e){this.clearSelections();},onContextMenu:function(e){var item=e.getTarget(this.itemSelector,this.getTemplateTarget());if(item){this.fireEvent("contextmenu",this,this.indexOf(item),item,e);}else{this.fireEvent("containercontextmenu",this,e);}},onDblClick:function(e){var item=e.getTarget(this.itemSelector,this.getTemplateTarget());if(item){this.fireEvent("dblclick",this,this.indexOf(item),item,e);}},onMouseOver:function(e){var item=e.getTarget(this.itemSelector,this.getTemplateTarget());if(item&&item!==this.lastItem){this.lastItem=item;Ext.fly(item).addClass(this.overClass);this.fireEvent("mouseenter",this,this.indexOf(item),item,e);}},onMouseOut:function(e){if(this.lastItem){if(!e.within(this.lastItem,true,true)){Ext.fly(this.lastItem).removeClass(this.overClass);this.fireEvent("mouseleave",this,this.indexOf(this.lastItem),this.lastItem,e);delete this.lastItem;}}},onItemClick:function(item,index,e){if(this.fireEvent("beforeclick",this,index,item,e)===false){return false;} +if(this.multiSelect){this.doMultiSelection(item,index,e);e.preventDefault();}else if(this.singleSelect){this.doSingleSelection(item,index,e);e.preventDefault();} +return true;},doSingleSelection:function(item,index,e){if(e.ctrlKey&&this.isSelected(index)){this.deselect(index);}else{this.select(index,false);}},doMultiSelection:function(item,index,e){if(e.shiftKey&&this.last!==false){var last=this.last;this.selectRange(last,index,e.ctrlKey);this.last=last;}else{if((e.ctrlKey||this.simpleSelect)&&this.isSelected(index)){this.deselect(index);}else{this.select(index,e.ctrlKey||e.shiftKey||this.simpleSelect);}}},getSelectionCount:function(){return this.selected.getCount();},getSelectedNodes:function(){return this.selected.elements;},getSelectedIndexes:function(){var indexes=[],selected=this.selected.elements,i=0,len=selected.length;for(;i<len;i++){indexes.push(selected[i].viewIndex);} +return indexes;},getSelectedRecords:function(){return this.getRecords(this.selected.elements);},getRecords:function(nodes){var records=[],i=0,len=nodes.length;for(;i<len;i++){records[records.length]=this.store.getAt(nodes[i].viewIndex);} +return records;},getRecord:function(node){return this.store.getAt(node.viewIndex);},clearSelections:function(suppressEvent,skipUpdate){if((this.multiSelect||this.singleSelect)&&this.selected.getCount()>0){if(!skipUpdate){this.selected.removeClass(this.selectedClass);} +this.selected.clear();this.last=false;if(!suppressEvent){this.fireEvent("selectionchange",this,this.selected.elements);}}},isSelected:function(node){return this.selected.contains(this.getNode(node));},deselect:function(node){if(this.isSelected(node)){node=this.getNode(node);this.selected.removeElement(node);if(this.last==node.viewIndex){this.last=false;} +Ext.fly(node).removeClass(this.selectedClass);this.fireEvent("selectionchange",this,this.selected.elements);}},select:function(nodeInfo,keepExisting,suppressEvent){if(Ext.isArray(nodeInfo)){if(!keepExisting){this.clearSelections(true);} +for(var i=0,len=nodeInfo.length;i<len;i++){this.select(nodeInfo[i],true,true);} +if(!suppressEvent){this.fireEvent("selectionchange",this,this.selected.elements);}}else{var node=this.getNode(nodeInfo);if(!keepExisting){this.clearSelections(true);} +if(node&&!this.isSelected(node)){if(this.fireEvent("beforeselect",this,node,this.selected.elements)!==false){Ext.fly(node).addClass(this.selectedClass);this.selected.add(node);this.last=node.viewIndex;if(!suppressEvent){this.fireEvent("selectionchange",this,this.selected.elements);}}}}},selectRange:function(start,end,keepExisting){if(!keepExisting){this.clearSelections(true);} +this.select(this.getNodes(start,end),true);},getNode:function(nodeInfo){if(Ext.isString(nodeInfo)){return document.getElementById(nodeInfo);}else if(Ext.isNumber(nodeInfo)){return this.all.elements[nodeInfo];}else if(nodeInfo instanceof Ext.data.Record){var idx=this.store.indexOf(nodeInfo);return this.all.elements[idx];} +return nodeInfo;},getNodes:function(start,end){var ns=this.all.elements,nodes=[],i;start=start||0;end=!Ext.isDefined(end)?Math.max(ns.length-1,0):end;if(start<=end){for(i=start;i<=end&&ns[i];i++){nodes.push(ns[i]);}}else{for(i=start;i>=end&&ns[i];i--){nodes.push(ns[i]);}} +return nodes;},indexOf:function(node){node=this.getNode(node);if(Ext.isNumber(node.viewIndex)){return node.viewIndex;} +return this.all.indexOf(node);},onBeforeLoad:function(){if(this.loadingText){this.clearSelections(false,true);this.getTemplateTarget().update('<div class="loading-indicator">'+this.loadingText+'</div>');this.all.clear();}},onDestroy:function(){this.all.clear();this.selected.clear();Ext.DataView.superclass.onDestroy.call(this);this.bindStore(null);}});Ext.DataView.prototype.setStore=Ext.DataView.prototype.bindStore;Ext.reg('dataview',Ext.DataView);Ext.list.ListView=Ext.extend(Ext.DataView,{itemSelector:'dl',selectedClass:'x-list-selected',overClass:'x-list-over',scrollOffset:undefined,columnResize:true,columnSort:true,maxColumnWidth:Ext.isIE?99:100,initComponent:function(){if(this.columnResize){this.colResizer=new Ext.list.ColumnResizer(this.colResizer);this.colResizer.init(this);} +if(this.columnSort){this.colSorter=new Ext.list.Sorter(this.columnSort);this.colSorter.init(this);} +if(!this.internalTpl){this.internalTpl=new Ext.XTemplate('<div class="x-list-header"><div class="x-list-header-inner">','<tpl for="columns">','<div style="width:{[values.width*100]}%;text-align:{align};"><em unselectable="on" id="',this.id,'-xlhd-{#}">','{header}','</em></div>','</tpl>','<div class="x-clear"></div>','</div></div>','<div class="x-list-body"><div class="x-list-body-inner">','</div></div>');} +if(!this.tpl){this.tpl=new Ext.XTemplate('<tpl for="rows">','<dl>','<tpl for="parent.columns">','<dt style="width:{[values.width*100]}%;text-align:{align};">','<em unselectable="on"<tpl if="cls"> class="{cls}</tpl>">','{[values.tpl.apply(parent)]}','</em></dt>','</tpl>','<div class="x-clear"></div>','</dl>','</tpl>');};var cs=this.columns,allocatedWidth=0,colsWithWidth=0,len=cs.length,columns=[];for(var i=0;i<len;i++){var c=cs[i];if(!c.isColumn){c.xtype=c.xtype?(/^lv/.test(c.xtype)?c.xtype:'lv'+c.xtype):'lvcolumn';c=Ext.create(c);} +if(c.width){allocatedWidth+=c.width*100;if(allocatedWidth>this.maxColumnWidth){c.width-=(allocatedWidth-this.maxColumnWidth)/100;} +colsWithWidth++;} +columns.push(c);} +cs=this.columns=columns;if(colsWithWidth<len){var remaining=len-colsWithWidth;if(allocatedWidth<this.maxColumnWidth){var perCol=((this.maxColumnWidth-allocatedWidth)/remaining)/100;for(var j=0;j<len;j++){var c=cs[j];if(!c.width){c.width=perCol;}}}} +Ext.list.ListView.superclass.initComponent.call(this);},onRender:function(){this.autoEl={cls:'x-list-wrap'};Ext.list.ListView.superclass.onRender.apply(this,arguments);this.internalTpl.overwrite(this.el,{columns:this.columns});this.innerBody=Ext.get(this.el.dom.childNodes[1].firstChild);this.innerHd=Ext.get(this.el.dom.firstChild.firstChild);if(this.hideHeaders){this.el.dom.firstChild.style.display='none';}},getTemplateTarget:function(){return this.innerBody;},collectData:function(){var rs=Ext.list.ListView.superclass.collectData.apply(this,arguments);return{columns:this.columns,rows:rs};},verifyInternalSize:function(){if(this.lastSize){this.onResize(this.lastSize.width,this.lastSize.height);}},onResize:function(w,h){var body=this.innerBody.dom,header=this.innerHd.dom,scrollWidth=w-Ext.num(this.scrollOffset,Ext.getScrollBarWidth())+'px',parentNode;if(!body){return;} +parentNode=body.parentNode;if(Ext.isNumber(w)){if(this.reserveScrollOffset||((parentNode.offsetWidth-parentNode.clientWidth)>10)){body.style.width=scrollWidth;header.style.width=scrollWidth;}else{body.style.width=w+'px';header.style.width=w+'px';setTimeout(function(){if((parentNode.offsetWidth-parentNode.clientWidth)>10){body.style.width=scrollWidth;header.style.width=scrollWidth;}},10);}} +if(Ext.isNumber(h)){parentNode.style.height=Math.max(0,h-header.parentNode.offsetHeight)+'px';}},updateIndexes:function(){Ext.list.ListView.superclass.updateIndexes.apply(this,arguments);this.verifyInternalSize();},findHeaderIndex:function(header){header=header.dom||header;var parentNode=header.parentNode,children=parentNode.parentNode.childNodes,i=0,c;for(;c=children[i];i++){if(c==parentNode){return i;}} +return-1;},setHdWidths:function(){var els=this.innerHd.dom.getElementsByTagName('div'),i=0,columns=this.columns,len=columns.length;for(;i<len;i++){els[i].style.width=(columns[i].width*100)+'%';}}});Ext.reg('listview',Ext.list.ListView);Ext.ListView=Ext.list.ListView;Ext.list.Column=Ext.extend(Object,{isColumn:true,align:'left',header:'',width:null,cls:'',constructor:function(c){if(!c.tpl){c.tpl=new Ext.XTemplate('{'+c.dataIndex+'}');} +else if(Ext.isString(c.tpl)){c.tpl=new Ext.XTemplate(c.tpl);} +Ext.apply(this,c);}});Ext.reg('lvcolumn',Ext.list.Column);Ext.list.NumberColumn=Ext.extend(Ext.list.Column,{format:'0,000.00',constructor:function(c){c.tpl=c.tpl||new Ext.XTemplate('{'+c.dataIndex+':number("'+(c.format||this.format)+'")}');Ext.list.NumberColumn.superclass.constructor.call(this,c);}});Ext.reg('lvnumbercolumn',Ext.list.NumberColumn);Ext.list.DateColumn=Ext.extend(Ext.list.Column,{format:'m/d/Y',constructor:function(c){c.tpl=c.tpl||new Ext.XTemplate('{'+c.dataIndex+':date("'+(c.format||this.format)+'")}');Ext.list.DateColumn.superclass.constructor.call(this,c);}});Ext.reg('lvdatecolumn',Ext.list.DateColumn);Ext.list.BooleanColumn=Ext.extend(Ext.list.Column,{trueText:'true',falseText:'false',undefinedText:' ',constructor:function(c){c.tpl=c.tpl||new Ext.XTemplate('{'+c.dataIndex+':this.format}');var t=this.trueText,f=this.falseText,u=this.undefinedText;c.tpl.format=function(v){if(v===undefined){return u;} +if(!v||v==='false'){return f;} +return t;};Ext.list.DateColumn.superclass.constructor.call(this,c);}});Ext.reg('lvbooleancolumn',Ext.list.BooleanColumn);Ext.list.ColumnResizer=Ext.extend(Ext.util.Observable,{minPct:.05,constructor:function(config){Ext.apply(this,config);Ext.list.ColumnResizer.superclass.constructor.call(this);},init:function(listView){this.view=listView;listView.on('render',this.initEvents,this);},initEvents:function(view){view.mon(view.innerHd,'mousemove',this.handleHdMove,this);this.tracker=new Ext.dd.DragTracker({onBeforeStart:this.onBeforeStart.createDelegate(this),onStart:this.onStart.createDelegate(this),onDrag:this.onDrag.createDelegate(this),onEnd:this.onEnd.createDelegate(this),tolerance:3,autoStart:300});this.tracker.initEl(view.innerHd);view.on('beforedestroy',this.tracker.destroy,this.tracker);},handleHdMove:function(e,t){var handleWidth=5,x=e.getPageX(),header=e.getTarget('em',3,true);if(header){var region=header.getRegion(),style=header.dom.style,parentNode=header.dom.parentNode;if(x-region.left<=handleWidth&&parentNode!=parentNode.parentNode.firstChild){this.activeHd=Ext.get(parentNode.previousSibling.firstChild);style.cursor=Ext.isWebKit?'e-resize':'col-resize';}else if(region.right-x<=handleWidth&&parentNode!=parentNode.parentNode.lastChild.previousSibling){this.activeHd=header;style.cursor=Ext.isWebKit?'w-resize':'col-resize';}else{delete this.activeHd;style.cursor='';}}},onBeforeStart:function(e){this.dragHd=this.activeHd;return!!this.dragHd;},onStart:function(e){var me=this,view=me.view,dragHeader=me.dragHd,x=me.tracker.getXY()[0];me.proxy=view.el.createChild({cls:'x-list-resizer'});me.dragX=dragHeader.getX();me.headerIndex=view.findHeaderIndex(dragHeader);me.headersDisabled=view.disableHeaders;view.disableHeaders=true;me.proxy.setHeight(view.el.getHeight());me.proxy.setX(me.dragX);me.proxy.setWidth(x-me.dragX);this.setBoundaries();},setBoundaries:function(relativeX){var view=this.view,headerIndex=this.headerIndex,width=view.innerHd.getWidth(),relativeX=view.innerHd.getX(),minWidth=Math.ceil(width*this.minPct),maxWidth=width-minWidth,numColumns=view.columns.length,headers=view.innerHd.select('em',true),minX=minWidth+relativeX,maxX=maxWidth+relativeX,header;if(numColumns==2){this.minX=minX;this.maxX=maxX;}else{header=headers.item(headerIndex+2);this.minX=headers.item(headerIndex).getX()+minWidth;this.maxX=header?header.getX()-minWidth:maxX;if(headerIndex==0){this.minX=minX;}else if(headerIndex==numColumns-2){this.maxX=maxX;}}},onDrag:function(e){var me=this,cursorX=me.tracker.getXY()[0].constrain(me.minX,me.maxX);me.proxy.setWidth(cursorX-this.dragX);},onEnd:function(e){var newWidth=this.proxy.getWidth(),index=this.headerIndex,view=this.view,columns=view.columns,width=view.innerHd.getWidth(),newPercent=Math.ceil(newWidth*view.maxColumnWidth/width)/100,disabled=this.headersDisabled,headerCol=columns[index],otherCol=columns[index+1],totalPercent=headerCol.width+otherCol.width;this.proxy.remove();headerCol.width=newPercent;otherCol.width=totalPercent-newPercent;delete this.dragHd;view.setHdWidths();view.refresh();setTimeout(function(){view.disableHeaders=disabled;},100);}});Ext.ListView.ColumnResizer=Ext.list.ColumnResizer;Ext.list.Sorter=Ext.extend(Ext.util.Observable,{sortClasses:["sort-asc","sort-desc"],constructor:function(config){Ext.apply(this,config);Ext.list.Sorter.superclass.constructor.call(this);},init:function(listView){this.view=listView;listView.on('render',this.initEvents,this);},initEvents:function(view){view.mon(view.innerHd,'click',this.onHdClick,this);view.innerHd.setStyle('cursor','pointer');view.mon(view.store,'datachanged',this.updateSortState,this);this.updateSortState.defer(10,this,[view.store]);},updateSortState:function(store){var state=store.getSortState();if(!state){return;} +this.sortState=state;var cs=this.view.columns,sortColumn=-1;for(var i=0,len=cs.length;i<len;i++){if(cs[i].dataIndex==state.field){sortColumn=i;break;}} +if(sortColumn!=-1){var sortDir=state.direction;this.updateSortIcon(sortColumn,sortDir);}},updateSortIcon:function(col,dir){var sc=this.sortClasses;var hds=this.view.innerHd.select('em').removeClass(sc);hds.item(col).addClass(sc[dir=="DESC"?1:0]);},onHdClick:function(e){var hd=e.getTarget('em',3);if(hd&&!this.view.disableHeaders){var index=this.view.findHeaderIndex(hd);this.view.store.sort(this.view.columns[index].dataIndex);}}});Ext.ListView.Sorter=Ext.list.Sorter;Ext.DataView.LabelEditor=Ext.extend(Ext.Editor,{alignment:"tl-tl",hideEl:false,cls:"x-small-editor",shim:false,completeOnEnter:true,cancelOnEsc:true,labelSelector:'span.x-editable',constructor:function(cfg,field){Ext.DataView.LabelEditor.superclass.constructor.call(this,field||new Ext.form.TextField({allowBlank:false,growMin:90,growMax:240,grow:true,selectOnFocus:true}),cfg);},init:function(view){this.view=view;view.on('render',this.initEditor,this);this.on('complete',this.onSave,this);},initEditor:function(){this.view.on({scope:this,containerclick:this.doBlur,click:this.doBlur});this.view.getEl().on('mousedown',this.onMouseDown,this,{delegate:this.labelSelector});},doBlur:function(){if(this.editing){this.field.blur();}},onMouseDown:function(e,target){if(!e.ctrlKey&&!e.shiftKey){var item=this.view.findItemFromChild(target);e.stopEvent();var record=this.view.store.getAt(this.view.indexOf(item));this.startEdit(target,record.data[this.dataIndex]);this.activeRecord=record;}else{e.preventDefault();}},onSave:function(ed,value){this.activeRecord.set(this.dataIndex,value);}});Ext.DataView.DragSelector=function(cfg){cfg=cfg||{};var view,proxy,tracker;var rs,bodyRegion,dragRegion=new Ext.lib.Region(0,0,0,0);var dragSafe=cfg.dragSafe===true;this.init=function(dataView){view=dataView;view.on('render',onRender);};function fillRegions(){rs=[];view.all.each(function(el){rs[rs.length]=el.getRegion();});bodyRegion=view.el.getRegion();} +function cancelClick(){return false;} +function onBeforeStart(e){return!dragSafe||e.target==view.el.dom;} +function onStart(e){view.on('containerclick',cancelClick,view,{single:true});if(!proxy){proxy=view.el.createChild({cls:'x-view-selector'});}else{if(proxy.dom.parentNode!==view.el.dom){view.el.dom.appendChild(proxy.dom);} +proxy.setDisplayed('block');} +fillRegions();view.clearSelections();} +function onDrag(e){var startXY=tracker.startXY;var xy=tracker.getXY();var x=Math.min(startXY[0],xy[0]);var y=Math.min(startXY[1],xy[1]);var w=Math.abs(startXY[0]-xy[0]);var h=Math.abs(startXY[1]-xy[1]);dragRegion.left=x;dragRegion.top=y;dragRegion.right=x+w;dragRegion.bottom=y+h;dragRegion.constrainTo(bodyRegion);proxy.setRegion(dragRegion);for(var i=0,len=rs.length;i<len;i++){var r=rs[i],sel=dragRegion.intersect(r);if(sel&&!r.selected){r.selected=true;view.select(i,true);}else if(!sel&&r.selected){r.selected=false;view.deselect(i);}}} +function onEnd(e){if(!Ext.isIE){view.un('containerclick',cancelClick,view);} +if(proxy){proxy.setDisplayed(false);}} +function onRender(view){tracker=new Ext.dd.DragTracker({onBeforeStart:onBeforeStart,onStart:onStart,onDrag:onDrag,onEnd:onEnd});tracker.initEl(view.el);}};Ext.data.Api=(function(){var validActions={};return{actions:{create:'create',read:'read',update:'update',destroy:'destroy'},restActions:{create:'POST',read:'GET',update:'PUT',destroy:'DELETE'},isAction:function(action){return(Ext.data.Api.actions[action])?true:false;},getVerb:function(name){if(validActions[name]){return validActions[name];} +for(var verb in this.actions){if(this.actions[verb]===name){validActions[name]=verb;break;}} +return(validActions[name]!==undefined)?validActions[name]:null;},isValid:function(api){var invalid=[];var crud=this.actions;for(var action in api){if(!(action in crud)){invalid.push(action);}} +return(!invalid.length)?true:invalid;},hasUniqueUrl:function(proxy,verb){var url=(proxy.api[verb])?proxy.api[verb].url:null;var unique=true;for(var action in proxy.api){if((unique=(action===verb)?true:(proxy.api[action].url!=url)?true:false)===false){break;}} +return unique;},prepare:function(proxy){if(!proxy.api){proxy.api={};} +for(var verb in this.actions){var action=this.actions[verb];proxy.api[action]=proxy.api[action]||proxy.url||proxy.directFn;if(typeof(proxy.api[action])=='string'){proxy.api[action]={url:proxy.api[action],method:(proxy.restful===true)?Ext.data.Api.restActions[action]:undefined};}}},restify:function(proxy){proxy.restful=true;for(var verb in this.restActions){proxy.api[this.actions[verb]].method||(proxy.api[this.actions[verb]].method=this.restActions[verb]);} +proxy.onWrite=proxy.onWrite.createInterceptor(function(action,o,response,rs){var reader=o.reader;var res=new Ext.data.Response({action:action,raw:response});switch(response.status){case 200:return true;break;case 201:if(Ext.isEmpty(res.raw.responseText)){res.success=true;}else{return true;} +break;case 204:res.success=true;res.data=null;break;default:return true;break;} +if(res.success===true){this.fireEvent("write",this,action,res.data,res,rs,o.request.arg);}else{this.fireEvent('exception',this,'remote',action,o,res,rs);} +o.request.callback.call(o.request.scope,res.data,res,res.success);return false;},proxy);}};})();Ext.data.Response=function(params,response){Ext.apply(this,params,{raw:response});};Ext.data.Response.prototype={message:null,success:false,status:null,root:null,raw:null,getMessage:function(){return this.message;},getSuccess:function(){return this.success;},getStatus:function(){return this.status;},getRoot:function(){return this.root;},getRawResponse:function(){return this.raw;}};Ext.data.Api.Error=Ext.extend(Ext.Error,{constructor:function(message,arg){this.arg=arg;Ext.Error.call(this,message);},name:'Ext.data.Api'});Ext.apply(Ext.data.Api.Error.prototype,{lang:{'action-url-undefined':'No fallback url defined for this action. When defining a DataProxy api, please be sure to define an url for each CRUD action in Ext.data.Api.actions or define a default url in addition to your api-configuration.','invalid':'received an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions defined in Ext.data.Api.actions','invalid-url':'Invalid url. Please review your proxy configuration.','execute':'Attempted to execute an unknown action. Valid API actions are defined in Ext.data.Api.actions"'}});Ext.data.SortTypes={none:function(s){return s;},stripTagsRE:/<\/?[^>]+>/gi,asText:function(s){return String(s).replace(this.stripTagsRE,"");},asUCText:function(s){return String(s).toUpperCase().replace(this.stripTagsRE,"");},asUCString:function(s){return String(s).toUpperCase();},asDate:function(s){if(!s){return 0;} +if(Ext.isDate(s)){return s.getTime();} +return Date.parse(String(s));},asFloat:function(s){var val=parseFloat(String(s).replace(/,/g,""));return isNaN(val)?0:val;},asInt:function(s){var val=parseInt(String(s).replace(/,/g,""),10);return isNaN(val)?0:val;}};Ext.data.Record=function(data,id){this.id=(id||id===0)?id:Ext.data.Record.id(this);this.data=data||{};};Ext.data.Record.create=function(o){var f=Ext.extend(Ext.data.Record,{});var p=f.prototype;p.fields=new Ext.util.MixedCollection(false,function(field){return field.name;});for(var i=0,len=o.length;i<len;i++){p.fields.add(new Ext.data.Field(o[i]));} +f.getField=function(name){return p.fields.get(name);};return f;};Ext.data.Record.PREFIX='ext-record';Ext.data.Record.AUTO_ID=1;Ext.data.Record.EDIT='edit';Ext.data.Record.REJECT='reject';Ext.data.Record.COMMIT='commit';Ext.data.Record.id=function(rec){rec.phantom=true;return[Ext.data.Record.PREFIX,'-',Ext.data.Record.AUTO_ID++].join('');};Ext.data.Record.prototype={dirty:false,editing:false,error:null,modified:null,phantom:false,join:function(store){this.store=store;},set:function(name,value){var encode=Ext.isPrimitive(value)?String:Ext.encode;if(encode(this.data[name])==encode(value)){return;} +this.dirty=true;if(!this.modified){this.modified={};} +if(this.modified[name]===undefined){this.modified[name]=this.data[name];} +this.data[name]=value;if(!this.editing){this.afterEdit();}},afterEdit:function(){if(this.store!=undefined&&typeof this.store.afterEdit=="function"){this.store.afterEdit(this);}},afterReject:function(){if(this.store){this.store.afterReject(this);}},afterCommit:function(){if(this.store){this.store.afterCommit(this);}},get:function(name){return this.data[name];},beginEdit:function(){this.editing=true;this.modified=this.modified||{};},cancelEdit:function(){this.editing=false;delete this.modified;},endEdit:function(){this.editing=false;if(this.dirty){this.afterEdit();}},reject:function(silent){var m=this.modified;for(var n in m){if(typeof m[n]!="function"){this.data[n]=m[n];}} +this.dirty=false;delete this.modified;this.editing=false;if(silent!==true){this.afterReject();}},commit:function(silent){this.dirty=false;delete this.modified;this.editing=false;if(silent!==true){this.afterCommit();}},getChanges:function(){var m=this.modified,cs={};for(var n in m){if(m.hasOwnProperty(n)){cs[n]=this.data[n];}} +return cs;},hasError:function(){return this.error!==null;},clearError:function(){this.error=null;},copy:function(newId){return new this.constructor(Ext.apply({},this.data),newId||this.id);},isModified:function(fieldName){return!!(this.modified&&this.modified.hasOwnProperty(fieldName));},isValid:function(){return this.fields.find(function(f){return(f.allowBlank===false&&Ext.isEmpty(this.data[f.name]))?true:false;},this)?false:true;},markDirty:function(){this.dirty=true;if(!this.modified){this.modified={};} +this.fields.each(function(f){this.modified[f.name]=this.data[f.name];},this);}};Ext.StoreMgr=Ext.apply(new Ext.util.MixedCollection(),{register:function(){for(var i=0,s;(s=arguments[i]);i++){this.add(s);}},unregister:function(){for(var i=0,s;(s=arguments[i]);i++){this.remove(this.lookup(s));}},lookup:function(id){if(Ext.isArray(id)){var fields=['field1'],expand=!Ext.isArray(id[0]);if(!expand){for(var i=2,len=id[0].length;i<=len;++i){fields.push('field'+i);}} +return new Ext.data.ArrayStore({fields:fields,data:id,expandData:expand,autoDestroy:true,autoCreated:true});} +return Ext.isObject(id)?(id.events?id:Ext.create(id,'store')):this.get(id);},getKey:function(o){return o.storeId;}});Ext.data.Store=Ext.extend(Ext.util.Observable,{writer:undefined,remoteSort:false,autoDestroy:false,pruneModifiedRecords:false,lastOptions:null,autoSave:true,batch:true,restful:false,paramNames:undefined,defaultParamNames:{start:'start',limit:'limit',sort:'sort',dir:'dir'},isDestroyed:false,hasMultiSort:false,batchKey:'_ext_batch_',constructor:function(config){this.data=new Ext.util.MixedCollection(false);this.data.getKey=function(o){return o.id;};this.removed=[];if(config&&config.data){this.inlineData=config.data;delete config.data;} +Ext.apply(this,config);this.baseParams=Ext.isObject(this.baseParams)?this.baseParams:{};this.paramNames=Ext.applyIf(this.paramNames||{},this.defaultParamNames);if((this.url||this.api)&&!this.proxy){this.proxy=new Ext.data.HttpProxy({url:this.url,api:this.api});} +if(this.restful===true&&this.proxy){this.batch=false;Ext.data.Api.restify(this.proxy);} +if(this.reader){if(!this.recordType){this.recordType=this.reader.recordType;} +if(this.reader.onMetaChange){this.reader.onMetaChange=this.reader.onMetaChange.createSequence(this.onMetaChange,this);} +if(this.writer){if(this.writer instanceof(Ext.data.DataWriter)===false){this.writer=this.buildWriter(this.writer);} +this.writer.meta=this.reader.meta;this.pruneModifiedRecords=true;}} +if(this.recordType){this.fields=this.recordType.prototype.fields;} +this.modified=[];this.addEvents('datachanged','metachange','add','remove','update','clear','exception','beforeload','load','loadexception','beforewrite','write','beforesave','save');if(this.proxy){this.relayEvents(this.proxy,['loadexception','exception']);} +if(this.writer){this.on({scope:this,add:this.createRecords,remove:this.destroyRecord,update:this.updateRecord,clear:this.onClear});} +this.sortToggle={};if(this.sortField){this.setDefaultSort(this.sortField,this.sortDir);}else if(this.sortInfo){this.setDefaultSort(this.sortInfo.field,this.sortInfo.direction);} +Ext.data.Store.superclass.constructor.call(this);if(this.id){this.storeId=this.id;delete this.id;} +if(this.storeId){Ext.StoreMgr.register(this);} +if(this.inlineData){this.loadData(this.inlineData);delete this.inlineData;}else if(this.autoLoad){this.load.defer(10,this,[typeof this.autoLoad=='object'?this.autoLoad:undefined]);} +this.batchCounter=0;this.batches={};},buildWriter:function(config){var klass=undefined,type=(config.format||'json').toLowerCase();switch(type){case'json':klass=Ext.data.JsonWriter;break;case'xml':klass=Ext.data.XmlWriter;break;default:klass=Ext.data.JsonWriter;} +return new klass(config);},destroy:function(){if(!this.isDestroyed){if(this.storeId){Ext.StoreMgr.unregister(this);} +this.clearData();this.data=null;Ext.destroy(this.proxy);this.reader=this.writer=null;this.purgeListeners();this.isDestroyed=true;}},add:function(records){var i,len,record,index;records=[].concat(records);if(records.length<1){return;} +for(i=0,len=records.length;i<len;i++){record=records[i];record.join(this);if(record.dirty||record.phantom){this.modified.push(record);}} +index=this.data.length;this.data.addAll(records);if(this.snapshot){this.snapshot.addAll(records);} +this.fireEvent('add',this,records,index);},addSorted:function(record){var index=this.findInsertIndex(record);this.insert(index,record);},doUpdate:function(rec){this.data.replace(rec.id,rec);if(this.snapshot){this.snapshot.replace(rec.id,rec);} +this.fireEvent('update',this,rec,Ext.data.Record.COMMIT);},remove:function(record){if(Ext.isArray(record)){Ext.each(record,function(r){this.remove(r);},this);return;} +var index=this.data.indexOf(record);if(index>-1){record.join(null);this.data.removeAt(index);} +if(this.pruneModifiedRecords){this.modified.remove(record);} +if(this.snapshot){this.snapshot.remove(record);} +if(index>-1){this.fireEvent('remove',this,record,index);}},removeAt:function(index){this.remove(this.getAt(index));},removeAll:function(silent){var items=[];this.each(function(rec){items.push(rec);});this.clearData();if(this.snapshot){this.snapshot.clear();} +if(this.pruneModifiedRecords){this.modified=[];} +if(silent!==true){this.fireEvent('clear',this,items);}},onClear:function(store,records){Ext.each(records,function(rec,index){this.destroyRecord(this,rec,index);},this);},insert:function(index,records){var i,len,record;records=[].concat(records);for(i=0,len=records.length;i<len;i++){record=records[i];this.data.insert(index+i,record);record.join(this);if(record.dirty||record.phantom){this.modified.push(record);}} +if(this.snapshot){this.snapshot.addAll(records);} +this.fireEvent('add',this,records,index);},indexOf:function(record){return this.data.indexOf(record);},indexOfId:function(id){return this.data.indexOfKey(id);},getById:function(id){return(this.snapshot||this.data).key(id);},getAt:function(index){return this.data.itemAt(index);},getRange:function(start,end){return this.data.getRange(start,end);},storeOptions:function(o){o=Ext.apply({},o);delete o.callback;delete o.scope;this.lastOptions=o;},clearData:function(){this.data.each(function(rec){rec.join(null);});this.data.clear();},load:function(options){options=Ext.apply({},options);this.storeOptions(options);if(this.sortInfo&&this.remoteSort){var pn=this.paramNames;options.params=Ext.apply({},options.params);options.params[pn.sort]=this.sortInfo.field;options.params[pn.dir]=this.sortInfo.direction;} +try{return this.execute('read',null,options);}catch(e){this.handleException(e);return false;}},updateRecord:function(store,record,action){if(action==Ext.data.Record.EDIT&&this.autoSave===true&&(!record.phantom||(record.phantom&&record.isValid()))){this.save();}},createRecords:function(store,records,index){var modified=this.modified,length=records.length,record,i;for(i=0;i<length;i++){record=records[i];if(record.phantom&&record.isValid()){record.markDirty();if(modified.indexOf(record)==-1){modified.push(record);}}} +if(this.autoSave===true){this.save();}},destroyRecord:function(store,record,index){if(this.modified.indexOf(record)!=-1){this.modified.remove(record);} +if(!record.phantom){this.removed.push(record);record.lastIndex=index;if(this.autoSave===true){this.save();}}},execute:function(action,rs,options,batch){if(!Ext.data.Api.isAction(action)){throw new Ext.data.Api.Error('execute',action);} +options=Ext.applyIf(options||{},{params:{}});if(batch!==undefined){this.addToBatch(batch);} +var doRequest=true;if(action==='read'){doRequest=this.fireEvent('beforeload',this,options);Ext.applyIf(options.params,this.baseParams);} +else{if(this.writer.listful===true&&this.restful!==true){rs=(Ext.isArray(rs))?rs:[rs];} +else if(Ext.isArray(rs)&&rs.length==1){rs=rs.shift();} +if((doRequest=this.fireEvent('beforewrite',this,action,rs,options))!==false){this.writer.apply(options.params,this.baseParams,action,rs);}} +if(doRequest!==false){if(this.writer&&this.proxy.url&&!this.proxy.restful&&!Ext.data.Api.hasUniqueUrl(this.proxy,action)){options.params.xaction=action;} +this.proxy.request(Ext.data.Api.actions[action],rs,options.params,this.reader,this.createCallback(action,rs,batch),this,options);} +return doRequest;},save:function(){if(!this.writer){throw new Ext.data.Store.Error('writer-undefined');} +var queue=[],len,trans,batch,data={},i;if(this.removed.length){queue.push(['destroy',this.removed]);} +var rs=[].concat(this.getModifiedRecords());if(rs.length){var phantoms=[];for(i=rs.length-1;i>=0;i--){if(rs[i].phantom===true){var rec=rs.splice(i,1).shift();if(rec.isValid()){phantoms.push(rec);}}else if(!rs[i].isValid()){rs.splice(i,1);}} +if(phantoms.length){queue.push(['create',phantoms]);} +if(rs.length){queue.push(['update',rs]);}} +len=queue.length;if(len){batch=++this.batchCounter;for(i=0;i<len;++i){trans=queue[i];data[trans[0]]=trans[1];} +if(this.fireEvent('beforesave',this,data)!==false){for(i=0;i<len;++i){trans=queue[i];this.doTransaction(trans[0],trans[1],batch);} +return batch;}} +return-1;},doTransaction:function(action,rs,batch){function transaction(records){try{this.execute(action,records,undefined,batch);}catch(e){this.handleException(e);}} +if(this.batch===false){for(var i=0,len=rs.length;i<len;i++){transaction.call(this,rs[i]);}}else{transaction.call(this,rs);}},addToBatch:function(batch){var b=this.batches,key=this.batchKey+batch,o=b[key];if(!o){b[key]=o={id:batch,count:0,data:{}};} +++o.count;},removeFromBatch:function(batch,action,data){var b=this.batches,key=this.batchKey+batch,o=b[key],arr;if(o){arr=o.data[action]||[];o.data[action]=arr.concat(data);if(o.count===1){data=o.data;delete b[key];this.fireEvent('save',this,batch,data);}else{--o.count;}}},createCallback:function(action,rs,batch){var actions=Ext.data.Api.actions;return(action=='read')?this.loadRecords:function(data,response,success){this['on'+Ext.util.Format.capitalize(action)+'Records'](success,rs,[].concat(data));if(success===true){this.fireEvent('write',this,action,data,response,rs);} +this.removeFromBatch(batch,action,data);};},clearModified:function(rs){if(Ext.isArray(rs)){for(var n=rs.length-1;n>=0;n--){this.modified.splice(this.modified.indexOf(rs[n]),1);}}else{this.modified.splice(this.modified.indexOf(rs),1);}},reMap:function(record){if(Ext.isArray(record)){for(var i=0,len=record.length;i<len;i++){this.reMap(record[i]);}}else{delete this.data.map[record._phid];this.data.map[record.id]=record;var index=this.data.keys.indexOf(record._phid);this.data.keys.splice(index,1,record.id);delete record._phid;}},onCreateRecords:function(success,rs,data){if(success===true){try{this.reader.realize(rs,data);this.reMap(rs);} +catch(e){this.handleException(e);if(Ext.isArray(rs)){this.onCreateRecords(success,rs,data);}}}},onUpdateRecords:function(success,rs,data){if(success===true){try{this.reader.update(rs,data);}catch(e){this.handleException(e);if(Ext.isArray(rs)){this.onUpdateRecords(success,rs,data);}}}},onDestroyRecords:function(success,rs,data){rs=(rs instanceof Ext.data.Record)?[rs]:[].concat(rs);for(var i=0,len=rs.length;i<len;i++){this.removed.splice(this.removed.indexOf(rs[i]),1);} +if(success===false){for(i=rs.length-1;i>=0;i--){this.insert(rs[i].lastIndex,rs[i]);}}},handleException:function(e){Ext.handleError(e);},reload:function(options){this.load(Ext.applyIf(options||{},this.lastOptions));},loadRecords:function(o,options,success){var i,len;if(this.isDestroyed===true){return;} +if(!o||success===false){if(success!==false){this.fireEvent('load',this,[],options);} +if(options.callback){options.callback.call(options.scope||this,[],options,false,o);} +return;} +var r=o.records,t=o.totalRecords||r.length;if(!options||options.add!==true){if(this.pruneModifiedRecords){this.modified=[];} +for(i=0,len=r.length;i<len;i++){r[i].join(this);} +if(this.snapshot){this.data=this.snapshot;delete this.snapshot;} +this.clearData();this.data.addAll(r);this.totalLength=t;this.applySort();this.fireEvent('datachanged',this);}else{var toAdd=[],rec,cnt=0;for(i=0,len=r.length;i<len;++i){rec=r[i];if(this.indexOfId(rec.id)>-1){this.doUpdate(rec);}else{toAdd.push(rec);++cnt;}} +this.totalLength=Math.max(t,this.data.length+cnt);this.add(toAdd);} +this.fireEvent('load',this,r,options);if(options.callback){options.callback.call(options.scope||this,r,options,true);}},loadData:function(o,append){var r=this.reader.readRecords(o);this.loadRecords(r,{add:append},true);},getCount:function(){return this.data.length||0;},getTotalCount:function(){return this.totalLength||0;},getSortState:function(){return this.sortInfo;},applySort:function(){if((this.sortInfo||this.multiSortInfo)&&!this.remoteSort){this.sortData();}},sortData:function(){var sortInfo=this.hasMultiSort?this.multiSortInfo:this.sortInfo,direction=sortInfo.direction||"ASC",sorters=sortInfo.sorters,sortFns=[];if(!this.hasMultiSort){sorters=[{direction:direction,field:sortInfo.field}];} +for(var i=0,j=sorters.length;i<j;i++){sortFns.push(this.createSortFunction(sorters[i].field,sorters[i].direction));} +if(sortFns.length==0){return;} +var directionModifier=direction.toUpperCase()=="DESC"?-1:1;var fn=function(r1,r2){var result=sortFns[0].call(this,r1,r2);if(sortFns.length>1){for(var i=1,j=sortFns.length;i<j;i++){result=result||sortFns[i].call(this,r1,r2);}} +return directionModifier*result;};this.data.sort(direction,fn);if(this.snapshot&&this.snapshot!=this.data){this.snapshot.sort(direction,fn);}},createSortFunction:function(field,direction){direction=direction||"ASC";var directionModifier=direction.toUpperCase()=="DESC"?-1:1;var sortType=this.fields.get(field).sortType;return function(r1,r2){var v1=sortType(r1.data[field]),v2=sortType(r2.data[field]);return directionModifier*(v1>v2?1:(v1<v2?-1:0));};},setDefaultSort:function(field,dir){dir=dir?dir.toUpperCase():'ASC';this.sortInfo={field:field,direction:dir};this.sortToggle[field]=dir;},sort:function(fieldName,dir){if(Ext.isArray(arguments[0])){return this.multiSort.call(this,fieldName,dir);}else{return this.singleSort(fieldName,dir);}},singleSort:function(fieldName,dir){var field=this.fields.get(fieldName);if(!field){return false;} +var name=field.name,sortInfo=this.sortInfo||null,sortToggle=this.sortToggle?this.sortToggle[name]:null;if(!dir){if(sortInfo&&sortInfo.field==name){dir=(this.sortToggle[name]||'ASC').toggle('ASC','DESC');}else{dir=field.sortDir;}} +this.sortToggle[name]=dir;this.sortInfo={field:name,direction:dir};this.hasMultiSort=false;if(this.remoteSort){if(!this.load(this.lastOptions)){if(sortToggle){this.sortToggle[name]=sortToggle;} +if(sortInfo){this.sortInfo=sortInfo;}}}else{this.applySort();this.fireEvent('datachanged',this);} +return true;},multiSort:function(sorters,direction){this.hasMultiSort=true;direction=direction||"ASC";if(this.multiSortInfo&&direction==this.multiSortInfo.direction){direction=direction.toggle("ASC","DESC");} +this.multiSortInfo={sorters:sorters,direction:direction};if(this.remoteSort){this.singleSort(sorters[0].field,sorters[0].direction);}else{this.applySort();this.fireEvent('datachanged',this);}},each:function(fn,scope){this.data.each(fn,scope);},getModifiedRecords:function(){return this.modified;},sum:function(property,start,end){var rs=this.data.items,v=0;start=start||0;end=(end||end===0)?end:rs.length-1;for(var i=start;i<=end;i++){v+=(rs[i].data[property]||0);} +return v;},createFilterFn:function(property,value,anyMatch,caseSensitive,exactMatch){if(Ext.isEmpty(value,false)){return false;} +value=this.data.createValueMatcher(value,anyMatch,caseSensitive,exactMatch);return function(r){return value.test(r.data[property]);};},createMultipleFilterFn:function(filters){return function(record){var isMatch=true;for(var i=0,j=filters.length;i<j;i++){var filter=filters[i],fn=filter.fn,scope=filter.scope;isMatch=isMatch&&fn.call(scope,record);} +return isMatch;};},filter:function(property,value,anyMatch,caseSensitive,exactMatch){var fn;if(Ext.isObject(property)){property=[property];} +if(Ext.isArray(property)){var filters=[];for(var i=0,j=property.length;i<j;i++){var filter=property[i],func=filter.fn,scope=filter.scope||this;if(!Ext.isFunction(func)){func=this.createFilterFn(filter.property,filter.value,filter.anyMatch,filter.caseSensitive,filter.exactMatch);} +filters.push({fn:func,scope:scope});} +fn=this.createMultipleFilterFn(filters);}else{fn=this.createFilterFn(property,value,anyMatch,caseSensitive,exactMatch);} +return fn?this.filterBy(fn):this.clearFilter();},filterBy:function(fn,scope){this.snapshot=this.snapshot||this.data;this.data=this.queryBy(fn,scope||this);this.fireEvent('datachanged',this);},clearFilter:function(suppressEvent){if(this.isFiltered()){this.data=this.snapshot;delete this.snapshot;if(suppressEvent!==true){this.fireEvent('datachanged',this);}}},isFiltered:function(){return!!this.snapshot&&this.snapshot!=this.data;},query:function(property,value,anyMatch,caseSensitive){var fn=this.createFilterFn(property,value,anyMatch,caseSensitive);return fn?this.queryBy(fn):this.data.clone();},queryBy:function(fn,scope){var data=this.snapshot||this.data;return data.filterBy(fn,scope||this);},find:function(property,value,start,anyMatch,caseSensitive){var fn=this.createFilterFn(property,value,anyMatch,caseSensitive);return fn?this.data.findIndexBy(fn,null,start):-1;},findExact:function(property,value,start){return this.data.findIndexBy(function(rec){return rec.get(property)===value;},this,start);},findBy:function(fn,scope,start){return this.data.findIndexBy(fn,scope,start);},collect:function(dataIndex,allowNull,bypassFilter){var d=(bypassFilter===true&&this.snapshot)?this.snapshot.items:this.data.items;var v,sv,r=[],l={};for(var i=0,len=d.length;i<len;i++){v=d[i].data[dataIndex];sv=String(v);if((allowNull||!Ext.isEmpty(v))&&!l[sv]){l[sv]=true;r[r.length]=v;}} +return r;},afterEdit:function(record){if(this.modified.indexOf(record)==-1){this.modified.push(record);} +this.fireEvent('update',this,record,Ext.data.Record.EDIT);},afterReject:function(record){this.modified.remove(record);this.fireEvent('update',this,record,Ext.data.Record.REJECT);},afterCommit:function(record){this.modified.remove(record);this.fireEvent('update',this,record,Ext.data.Record.COMMIT);},commitChanges:function(){var modified=this.modified.slice(0),length=modified.length,i;for(i=0;i<length;i++){modified[i].commit();} +this.modified=[];this.removed=[];},rejectChanges:function(){var modified=this.modified.slice(0),removed=this.removed.slice(0).reverse(),mLength=modified.length,rLength=removed.length,i;for(i=0;i<mLength;i++){modified[i].reject();} +for(i=0;i<rLength;i++){this.insert(removed[i].lastIndex||0,removed[i]);removed[i].reject();} +this.modified=[];this.removed=[];},onMetaChange:function(meta){this.recordType=this.reader.recordType;this.fields=this.recordType.prototype.fields;delete this.snapshot;if(this.reader.meta.sortInfo){this.sortInfo=this.reader.meta.sortInfo;}else if(this.sortInfo&&!this.fields.get(this.sortInfo.field)){delete this.sortInfo;} +if(this.writer){this.writer.meta=this.reader.meta;} +this.modified=[];this.fireEvent('metachange',this,this.reader.meta);},findInsertIndex:function(record){this.suspendEvents();var data=this.data.clone();this.data.add(record);this.applySort();var index=this.data.indexOf(record);this.data=data;this.resumeEvents();return index;},setBaseParam:function(name,value){this.baseParams=this.baseParams||{};this.baseParams[name]=value;}});Ext.reg('store',Ext.data.Store);Ext.data.Store.Error=Ext.extend(Ext.Error,{name:'Ext.data.Store'});Ext.apply(Ext.data.Store.Error.prototype,{lang:{'writer-undefined':'Attempted to execute a write-action without a DataWriter installed.'}});Ext.data.Field=Ext.extend(Object,{constructor:function(config){if(Ext.isString(config)){config={name:config};} +Ext.apply(this,config);var types=Ext.data.Types,st=this.sortType,t;if(this.type){if(Ext.isString(this.type)){this.type=Ext.data.Types[this.type.toUpperCase()]||types.AUTO;}}else{this.type=types.AUTO;} +if(Ext.isString(st)){this.sortType=Ext.data.SortTypes[st];}else if(Ext.isEmpty(st)){this.sortType=this.type.sortType;} +if(!this.convert){this.convert=this.type.convert;}},dateFormat:null,useNull:false,defaultValue:"",mapping:null,sortType:null,sortDir:"ASC",allowBlank:true});Ext.data.DataReader=function(meta,recordType){this.meta=meta;this.recordType=Ext.isArray(recordType)?Ext.data.Record.create(recordType):recordType;if(this.recordType){this.buildExtractors();}};Ext.data.DataReader.prototype={getTotal:Ext.emptyFn,getRoot:Ext.emptyFn,getMessage:Ext.emptyFn,getSuccess:Ext.emptyFn,getId:Ext.emptyFn,buildExtractors:Ext.emptyFn,extractValues:Ext.emptyFn,realize:function(rs,data){if(Ext.isArray(rs)){for(var i=rs.length-1;i>=0;i--){if(Ext.isArray(data)){this.realize(rs.splice(i,1).shift(),data.splice(i,1).shift());} +else{this.realize(rs.splice(i,1).shift(),data);}}} +else{if(Ext.isArray(data)&&data.length==1){data=data.shift();} +if(!this.isData(data)){throw new Ext.data.DataReader.Error('realize',rs);} +rs.phantom=false;rs._phid=rs.id;rs.id=this.getId(data);rs.data=data;rs.commit();}},update:function(rs,data){if(Ext.isArray(rs)){for(var i=rs.length-1;i>=0;i--){if(Ext.isArray(data)){this.update(rs.splice(i,1).shift(),data.splice(i,1).shift());} +else{this.update(rs.splice(i,1).shift(),data);}}} +else{if(Ext.isArray(data)&&data.length==1){data=data.shift();} +if(this.isData(data)){rs.data=Ext.apply(rs.data,data);} +rs.commit();}},extractData:function(root,returnRecords){var rawName=(this instanceof Ext.data.JsonReader)?'json':'node';var rs=[];if(this.isData(root)&&!(this instanceof Ext.data.XmlReader)){root=[root];} +var f=this.recordType.prototype.fields,fi=f.items,fl=f.length,rs=[];if(returnRecords===true){var Record=this.recordType;for(var i=0;i<root.length;i++){var n=root[i];var record=new Record(this.extractValues(n,fi,fl),this.getId(n));record[rawName]=n;rs.push(record);}} +else{for(var i=0;i<root.length;i++){var data=this.extractValues(root[i],fi,fl);data[this.meta.idProperty]=this.getId(root[i]);rs.push(data);}} +return rs;},isData:function(data){return(data&&Ext.isObject(data)&&!Ext.isEmpty(this.getId(data)))?true:false;},onMetaChange:function(meta){delete this.ef;this.meta=meta;this.recordType=Ext.data.Record.create(meta.fields);this.buildExtractors();}};Ext.data.DataReader.Error=Ext.extend(Ext.Error,{constructor:function(message,arg){this.arg=arg;Ext.Error.call(this,message);},name:'Ext.data.DataReader'});Ext.apply(Ext.data.DataReader.Error.prototype,{lang:{'update':"#update received invalid data from server. Please see docs for DataReader#update and review your DataReader configuration.",'realize':"#realize was called with invalid remote-data. Please see the docs for DataReader#realize and review your DataReader configuration.",'invalid-response':"#readResponse received an invalid response from the server."}});Ext.data.DataWriter=function(config){Ext.apply(this,config);};Ext.data.DataWriter.prototype={writeAllFields:false,listful:false,apply:function(params,baseParams,action,rs){var data=[],renderer=action+'Record';if(Ext.isArray(rs)){Ext.each(rs,function(rec){data.push(this[renderer](rec));},this);} +else if(rs instanceof Ext.data.Record){data=this[renderer](rs);} +this.render(params,baseParams,data);},render:Ext.emptyFn,updateRecord:Ext.emptyFn,createRecord:Ext.emptyFn,destroyRecord:Ext.emptyFn,toHash:function(rec,config){var map=rec.fields.map,data={},raw=(this.writeAllFields===false&&rec.phantom===false)?rec.getChanges():rec.data,m;Ext.iterate(raw,function(prop,value){if((m=map[prop])){data[m.mapping?m.mapping:m.name]=value;}});if(rec.phantom){if(rec.fields.containsKey(this.meta.idProperty)&&Ext.isEmpty(rec.data[this.meta.idProperty])){delete data[this.meta.idProperty];}}else{data[this.meta.idProperty]=rec.id;} +return data;},toArray:function(data){var fields=[];Ext.iterate(data,function(k,v){fields.push({name:k,value:v});},this);return fields;}};Ext.data.DataProxy=function(conn){conn=conn||{};this.api=conn.api;this.url=conn.url;this.restful=conn.restful;this.listeners=conn.listeners;this.prettyUrls=conn.prettyUrls;this.addEvents('exception','beforeload','load','loadexception','beforewrite','write');Ext.data.DataProxy.superclass.constructor.call(this);try{Ext.data.Api.prepare(this);}catch(e){if(e instanceof Ext.data.Api.Error){e.toConsole();}} +Ext.data.DataProxy.relayEvents(this,['beforewrite','write','exception']);};Ext.extend(Ext.data.DataProxy,Ext.util.Observable,{restful:false,setApi:function(){if(arguments.length==1){var valid=Ext.data.Api.isValid(arguments[0]);if(valid===true){this.api=arguments[0];} +else{throw new Ext.data.Api.Error('invalid',valid);}} +else if(arguments.length==2){if(!Ext.data.Api.isAction(arguments[0])){throw new Ext.data.Api.Error('invalid',arguments[0]);} +this.api[arguments[0]]=arguments[1];} +Ext.data.Api.prepare(this);},isApiAction:function(action){return(this.api[action])?true:false;},request:function(action,rs,params,reader,callback,scope,options){if(!this.api[action]&&!this.load){throw new Ext.data.DataProxy.Error('action-undefined',action);} +params=params||{};if((action===Ext.data.Api.actions.read)?this.fireEvent("beforeload",this,params):this.fireEvent("beforewrite",this,action,rs,params)!==false){this.doRequest.apply(this,arguments);} +else{callback.call(scope||this,null,options,false);}},load:null,doRequest:function(action,rs,params,reader,callback,scope,options){this.load(params,reader,callback,scope,options);},onRead:Ext.emptyFn,onWrite:Ext.emptyFn,buildUrl:function(action,record){record=record||null;var url=(this.conn&&this.conn.url)?this.conn.url:(this.api[action])?this.api[action].url:this.url;if(!url){throw new Ext.data.Api.Error('invalid-url',action);} +var provides=null;var m=url.match(/(.*)(\.json|\.xml|\.html)$/);if(m){provides=m[2];url=m[1];} +if((this.restful===true||this.prettyUrls===true)&&record instanceof Ext.data.Record&&!record.phantom){url+='/'+record.id;} +return(provides===null)?url:url+provides;},destroy:function(){this.purgeListeners();}});Ext.apply(Ext.data.DataProxy,Ext.util.Observable.prototype);Ext.util.Observable.call(Ext.data.DataProxy);Ext.data.DataProxy.Error=Ext.extend(Ext.Error,{constructor:function(message,arg){this.arg=arg;Ext.Error.call(this,message);},name:'Ext.data.DataProxy'});Ext.apply(Ext.data.DataProxy.Error.prototype,{lang:{'action-undefined':"DataProxy attempted to execute an API-action but found an undefined url / function. Please review your Proxy url/api-configuration.",'api-invalid':'Recieved an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions from Ext.data.Api.actions.'}});Ext.data.Request=function(params){Ext.apply(this,params);};Ext.data.Request.prototype={action:undefined,rs:undefined,params:undefined,callback:Ext.emptyFn,scope:undefined,reader:undefined};Ext.data.Response=function(params){Ext.apply(this,params);};Ext.data.Response.prototype={action:undefined,success:undefined,message:undefined,data:undefined,raw:undefined,records:undefined};Ext.data.ScriptTagProxy=function(config){Ext.apply(this,config);Ext.data.ScriptTagProxy.superclass.constructor.call(this,config);this.head=document.getElementsByTagName("head")[0];};Ext.data.ScriptTagProxy.TRANS_ID=1000;Ext.extend(Ext.data.ScriptTagProxy,Ext.data.DataProxy,{timeout:30000,callbackParam:"callback",nocache:true,doRequest:function(action,rs,params,reader,callback,scope,arg){var p=Ext.urlEncode(Ext.apply(params,this.extraParams));var url=this.buildUrl(action,rs);if(!url){throw new Ext.data.Api.Error('invalid-url',url);} +url=Ext.urlAppend(url,p);if(this.nocache){url=Ext.urlAppend(url,'_dc='+(new Date().getTime()));} +var transId=++Ext.data.ScriptTagProxy.TRANS_ID;var trans={id:transId,action:action,cb:"stcCallback"+transId,scriptId:"stcScript"+transId,params:params,arg:arg,url:url,callback:callback,scope:scope,reader:reader};window[trans.cb]=this.createCallback(action,rs,trans);url+=String.format("&{0}={1}",this.callbackParam,trans.cb);if(this.autoAbort!==false){this.abort();} +trans.timeoutId=this.handleFailure.defer(this.timeout,this,[trans]);var script=document.createElement("script");script.setAttribute("src",url);script.setAttribute("type","text/javascript");script.setAttribute("id",trans.scriptId);this.head.appendChild(script);this.trans=trans;},createCallback:function(action,rs,trans){var self=this;return function(res){self.trans=false;self.destroyTrans(trans,true);if(action===Ext.data.Api.actions.read){self.onRead.call(self,action,trans,res);}else{self.onWrite.call(self,action,trans,res,rs);}};},onRead:function(action,trans,res){var result;try{result=trans.reader.readRecords(res);}catch(e){this.fireEvent("loadexception",this,trans,res,e);this.fireEvent('exception',this,'response',action,trans,res,e);trans.callback.call(trans.scope||window,null,trans.arg,false);return;} +if(result.success===false){this.fireEvent('loadexception',this,trans,res);this.fireEvent('exception',this,'remote',action,trans,res,null);}else{this.fireEvent("load",this,res,trans.arg);} +trans.callback.call(trans.scope||window,result,trans.arg,result.success);},onWrite:function(action,trans,response,rs){var reader=trans.reader;try{var res=reader.readResponse(action,response);}catch(e){this.fireEvent('exception',this,'response',action,trans,res,e);trans.callback.call(trans.scope||window,null,res,false);return;} +if(!res.success===true){this.fireEvent('exception',this,'remote',action,trans,res,rs);trans.callback.call(trans.scope||window,null,res,false);return;} +this.fireEvent("write",this,action,res.data,res,rs,trans.arg);trans.callback.call(trans.scope||window,res.data,res,true);},isLoading:function(){return this.trans?true:false;},abort:function(){if(this.isLoading()){this.destroyTrans(this.trans);}},destroyTrans:function(trans,isLoaded){this.head.removeChild(document.getElementById(trans.scriptId));clearTimeout(trans.timeoutId);if(isLoaded){window[trans.cb]=undefined;try{delete window[trans.cb];}catch(e){}}else{window[trans.cb]=function(){window[trans.cb]=undefined;try{delete window[trans.cb];}catch(e){}};}},handleFailure:function(trans){this.trans=false;this.destroyTrans(trans,false);if(trans.action===Ext.data.Api.actions.read){this.fireEvent("loadexception",this,null,trans.arg);} +this.fireEvent('exception',this,'response',trans.action,{response:null,options:trans.arg});trans.callback.call(trans.scope||window,null,trans.arg,false);},destroy:function(){this.abort();Ext.data.ScriptTagProxy.superclass.destroy.call(this);}});Ext.data.HttpProxy=function(conn){Ext.data.HttpProxy.superclass.constructor.call(this,conn);this.conn=conn;this.conn.url=null;this.useAjax=!conn||!conn.events;var actions=Ext.data.Api.actions;this.activeRequest={};for(var verb in actions){this.activeRequest[actions[verb]]=undefined;}};Ext.extend(Ext.data.HttpProxy,Ext.data.DataProxy,{getConnection:function(){return this.useAjax?Ext.Ajax:this.conn;},setUrl:function(url,makePermanent){this.conn.url=url;if(makePermanent===true){this.url=url;this.api=null;Ext.data.Api.prepare(this);}},doRequest:function(action,rs,params,reader,cb,scope,arg){var o={method:(this.api[action])?this.api[action]['method']:undefined,request:{callback:cb,scope:scope,arg:arg},reader:reader,callback:this.createCallback(action,rs),scope:this};if(params.jsonData){o.jsonData=params.jsonData;}else if(params.xmlData){o.xmlData=params.xmlData;}else{o.params=params||{};} +this.conn.url=this.buildUrl(action,rs);if(this.useAjax){Ext.applyIf(o,this.conn);if(this.activeRequest[action]){} +this.activeRequest[action]=Ext.Ajax.request(o);}else{this.conn.request(o);} +this.conn.url=null;},createCallback:function(action,rs){return function(o,success,response){this.activeRequest[action]=undefined;if(!success){if(action===Ext.data.Api.actions.read){this.fireEvent('loadexception',this,o,response);} +this.fireEvent('exception',this,'response',action,o,response);o.request.callback.call(o.request.scope,null,o.request.arg,false);return;} +if(action===Ext.data.Api.actions.read){this.onRead(action,o,response);}else{this.onWrite(action,o,response,rs);}};},onRead:function(action,o,response){var result;try{result=o.reader.read(response);}catch(e){this.fireEvent('loadexception',this,o,response,e);this.fireEvent('exception',this,'response',action,o,response,e);o.request.callback.call(o.request.scope,null,o.request.arg,false);return;} +if(result.success===false){this.fireEvent('loadexception',this,o,response);var res=o.reader.readResponse(action,response);this.fireEvent('exception',this,'remote',action,o,res,null);} +else{this.fireEvent('load',this,o,o.request.arg);} +o.request.callback.call(o.request.scope,result,o.request.arg,result.success);},onWrite:function(action,o,response,rs){var reader=o.reader;var res;try{res=reader.readResponse(action,response);}catch(e){this.fireEvent('exception',this,'response',action,o,response,e);o.request.callback.call(o.request.scope,null,o.request.arg,false);return;} +if(res.success===true){this.fireEvent('write',this,action,res.data,res,rs,o.request.arg);}else{this.fireEvent('exception',this,'remote',action,o,res,rs);} +o.request.callback.call(o.request.scope,res.data,res,res.success);},destroy:function(){if(!this.useAjax){this.conn.abort();}else if(this.activeRequest){var actions=Ext.data.Api.actions;for(var verb in actions){if(this.activeRequest[actions[verb]]){Ext.Ajax.abort(this.activeRequest[actions[verb]]);}}} +Ext.data.HttpProxy.superclass.destroy.call(this);}});Ext.data.MemoryProxy=function(data){var api={};api[Ext.data.Api.actions.read]=true;Ext.data.MemoryProxy.superclass.constructor.call(this,{api:api});this.data=data;};Ext.extend(Ext.data.MemoryProxy,Ext.data.DataProxy,{doRequest:function(action,rs,params,reader,callback,scope,arg){params=params||{};var result;try{result=reader.readRecords(this.data);}catch(e){this.fireEvent("loadexception",this,null,arg,e);this.fireEvent('exception',this,'response',action,arg,null,e);callback.call(scope,null,arg,false);return;} +callback.call(scope,result,arg,true);}});Ext.data.Types=new function(){var st=Ext.data.SortTypes;Ext.apply(this,{stripRe:/[\$,%]/g,AUTO:{convert:function(v){return v;},sortType:st.none,type:'auto'},STRING:{convert:function(v){return(v===undefined||v===null)?'':String(v);},sortType:st.asUCString,type:'string'},INT:{convert:function(v){return v!==undefined&&v!==null&&v!==''?parseInt(String(v).replace(Ext.data.Types.stripRe,''),10):(this.useNull?null:0);},sortType:st.none,type:'int'},FLOAT:{convert:function(v){return v!==undefined&&v!==null&&v!==''?parseFloat(String(v).replace(Ext.data.Types.stripRe,''),10):(this.useNull?null:0);},sortType:st.none,type:'float'},BOOL:{convert:function(v){return v===true||v==='true'||v==1;},sortType:st.none,type:'bool'},DATE:{convert:function(v){var df=this.dateFormat;if(!v){return null;} +if(Ext.isDate(v)){return v;} +if(df){if(df=='timestamp'){return new Date(v*1000);} +if(df=='time'){return new Date(parseInt(v,10));} +return Date.parseDate(v,df);} +var parsed=Date.parse(v);return parsed?new Date(parsed):null;},sortType:st.asDate,type:'date'}});Ext.apply(this,{BOOLEAN:this.BOOL,INTEGER:this.INT,NUMBER:this.FLOAT});};Ext.data.JsonWriter=Ext.extend(Ext.data.DataWriter,{encode:true,encodeDelete:false,constructor:function(config){Ext.data.JsonWriter.superclass.constructor.call(this,config);},render:function(params,baseParams,data){if(this.encode===true){Ext.apply(params,baseParams);params[this.meta.root]=Ext.encode(data);}else{var jdata=Ext.apply({},baseParams);jdata[this.meta.root]=data;params.jsonData=jdata;}},createRecord:function(rec){return this.toHash(rec);},updateRecord:function(rec){return this.toHash(rec);},destroyRecord:function(rec){if(this.encodeDelete){var data={};data[this.meta.idProperty]=rec.id;return data;}else{return rec.id;}}});Ext.data.JsonReader=function(meta,recordType){meta=meta||{};Ext.applyIf(meta,{idProperty:'id',successProperty:'success',totalProperty:'total'});Ext.data.JsonReader.superclass.constructor.call(this,meta,recordType||meta.fields);};Ext.extend(Ext.data.JsonReader,Ext.data.DataReader,{read:function(response){var json=response.responseText;var o=Ext.decode(json);if(!o){throw{message:'JsonReader.read: Json object not found'};} +return this.readRecords(o);},readResponse:function(action,response){var o=(response.responseText!==undefined)?Ext.decode(response.responseText):response;if(!o){throw new Ext.data.JsonReader.Error('response');} +var root=this.getRoot(o);if(action===Ext.data.Api.actions.create){var def=Ext.isDefined(root);if(def&&Ext.isEmpty(root)){throw new Ext.data.JsonReader.Error('root-empty',this.meta.root);} +else if(!def){throw new Ext.data.JsonReader.Error('root-undefined-response',this.meta.root);}} +var res=new Ext.data.Response({action:action,success:this.getSuccess(o),data:(root)?this.extractData(root,false):[],message:this.getMessage(o),raw:o});if(Ext.isEmpty(res.success)){throw new Ext.data.JsonReader.Error('successProperty-response',this.meta.successProperty);} +return res;},readRecords:function(o){this.jsonData=o;if(o.metaData){this.onMetaChange(o.metaData);} +var s=this.meta,Record=this.recordType,f=Record.prototype.fields,fi=f.items,fl=f.length,v;var root=this.getRoot(o),c=root.length,totalRecords=c,success=true;if(s.totalProperty){v=parseInt(this.getTotal(o),10);if(!isNaN(v)){totalRecords=v;}} +if(s.successProperty){v=this.getSuccess(o);if(v===false||v==='false'){success=false;}} +return{success:success,records:this.extractData(root,true),totalRecords:totalRecords};},buildExtractors:function(){if(this.ef){return;} +var s=this.meta,Record=this.recordType,f=Record.prototype.fields,fi=f.items,fl=f.length;if(s.totalProperty){this.getTotal=this.createAccessor(s.totalProperty);} +if(s.successProperty){this.getSuccess=this.createAccessor(s.successProperty);} +if(s.messageProperty){this.getMessage=this.createAccessor(s.messageProperty);} +this.getRoot=s.root?this.createAccessor(s.root):function(p){return p;};if(s.id||s.idProperty){var g=this.createAccessor(s.id||s.idProperty);this.getId=function(rec){var r=g(rec);return(r===undefined||r==='')?null:r;};}else{this.getId=function(){return null;};} +var ef=[];for(var i=0;i<fl;i++){f=fi[i];var map=(f.mapping!==undefined&&f.mapping!==null)?f.mapping:f.name;ef.push(this.createAccessor(map));} +this.ef=ef;},simpleAccess:function(obj,subsc){return obj[subsc];},createAccessor:function(){var re=/[\[\.]/;return function(expr){if(Ext.isEmpty(expr)){return Ext.emptyFn;} +if(Ext.isFunction(expr)){return expr;} +var i=String(expr).search(re);if(i>=0){return new Function('obj','return obj'+(i>0?'.':'')+expr);} +return function(obj){return obj[expr];};};}(),extractValues:function(data,items,len){var f,values={};for(var j=0;j<len;j++){f=items[j];var v=this.ef[j](data);values[f.name]=f.convert((v!==undefined)?v:f.defaultValue,data);} +return values;}});Ext.data.JsonReader.Error=Ext.extend(Ext.Error,{constructor:function(message,arg){this.arg=arg;Ext.Error.call(this,message);},name:'Ext.data.JsonReader'});Ext.apply(Ext.data.JsonReader.Error.prototype,{lang:{'response':'An error occurred while json-decoding your server response','successProperty-response':'Could not locate your "successProperty" in your server response. Please review your JsonReader config to ensure the config-property "successProperty" matches the property in your server-response. See the JsonReader docs.','root-undefined-config':'Your JsonReader was configured without a "root" property. Please review your JsonReader config and make sure to define the root property. See the JsonReader docs.','idProperty-undefined':'Your JsonReader was configured without an "idProperty" Please review your JsonReader configuration and ensure the "idProperty" is set (e.g.: "id"). See the JsonReader docs.','root-empty':'Data was expected to be returned by the server in the "root" property of the response. Please review your JsonReader configuration to ensure the "root" property matches that returned in the server-response. See JsonReader docs.'}});Ext.data.ArrayReader=Ext.extend(Ext.data.JsonReader,{readRecords:function(o){this.arrayData=o;var s=this.meta,sid=s?Ext.num(s.idIndex,s.id):null,recordType=this.recordType,fields=recordType.prototype.fields,records=[],success=true,v;var root=this.getRoot(o);for(var i=0,len=root.length;i<len;i++){var n=root[i],values={},id=((sid||sid===0)&&n[sid]!==undefined&&n[sid]!==""?n[sid]:null);for(var j=0,jlen=fields.length;j<jlen;j++){var f=fields.items[j],k=f.mapping!==undefined&&f.mapping!==null?f.mapping:j;v=n[k]!==undefined?n[k]:f.defaultValue;v=f.convert(v,n);values[f.name]=v;} +var record=new recordType(values,id);record.json=n;records[records.length]=record;} +var totalRecords=records.length;if(s.totalProperty){v=parseInt(this.getTotal(o),10);if(!isNaN(v)){totalRecords=v;}} +if(s.successProperty){v=this.getSuccess(o);if(v===false||v==='false'){success=false;}} +return{success:success,records:records,totalRecords:totalRecords};}});Ext.data.ArrayStore=Ext.extend(Ext.data.Store,{constructor:function(config){Ext.data.ArrayStore.superclass.constructor.call(this,Ext.apply(config,{reader:new Ext.data.ArrayReader(config)}));},loadData:function(data,append){if(this.expandData===true){var r=[];for(var i=0,len=data.length;i<len;i++){r[r.length]=[data[i]];} +data=r;} +Ext.data.ArrayStore.superclass.loadData.call(this,data,append);}});Ext.reg('arraystore',Ext.data.ArrayStore);Ext.data.SimpleStore=Ext.data.ArrayStore;Ext.reg('simplestore',Ext.data.SimpleStore);Ext.data.JsonStore=Ext.extend(Ext.data.Store,{constructor:function(config){Ext.data.JsonStore.superclass.constructor.call(this,Ext.apply(config,{reader:new Ext.data.JsonReader(config)}));}});Ext.reg('jsonstore',Ext.data.JsonStore);Ext.form.Field=Ext.extend(Ext.BoxComponent,{invalidClass:'x-form-invalid',invalidText:'The value in this field is invalid',focusClass:'x-form-focus',validationEvent:'keyup',validateOnBlur:true,validationDelay:250,defaultAutoCreate:{tag:'input',type:'text',size:'20',autocomplete:'off'},fieldClass:'x-form-field',msgTarget:'qtip',msgFx:'normal',readOnly:false,disabled:false,submitValue:true,isFormField:true,msgDisplay:'',hasFocus:false,initComponent:function(){Ext.form.Field.superclass.initComponent.call(this);this.addEvents('focus','blur','specialkey','change','invalid','valid');},getName:function(){return this.rendered&&this.el.dom.name?this.el.dom.name:this.name||this.id||'';},onRender:function(ct,position){if(!this.el){var cfg=this.getAutoCreate();if(!cfg.name){cfg.name=this.name||this.id;} +if(this.inputType){cfg.type=this.inputType;} +this.autoEl=cfg;} +Ext.form.Field.superclass.onRender.call(this,ct,position);if(this.submitValue===false){this.el.dom.removeAttribute('name');} +var type=this.el.dom.type;if(type){if(type=='password'){type='text';} +this.el.addClass('x-form-'+type);} +if(this.readOnly){this.setReadOnly(true);} +if(this.tabIndex!==undefined){this.el.dom.setAttribute('tabIndex',this.tabIndex);} +this.el.addClass([this.fieldClass,this.cls]);},getItemCt:function(){return this.itemCt;},initValue:function(){if(this.value!==undefined){this.setValue(this.value);}else if(!Ext.isEmpty(this.el.dom.value)&&this.el.dom.value!=this.emptyText){this.setValue(this.el.dom.value);} +this.originalValue=this.getValue();},isDirty:function(){if(this.disabled||!this.rendered){return false;} +return String(this.getValue())!==String(this.originalValue);},setReadOnly:function(readOnly){if(this.rendered){this.el.dom.readOnly=readOnly;} +this.readOnly=readOnly;},afterRender:function(){Ext.form.Field.superclass.afterRender.call(this);this.initEvents();this.initValue();},fireKey:function(e){if(e.isSpecialKey()){this.fireEvent('specialkey',this,e);}},reset:function(){this.setValue(this.originalValue);this.clearInvalid();},initEvents:function(){this.mon(this.el,Ext.EventManager.getKeyEvent(),this.fireKey,this);this.mon(this.el,'focus',this.onFocus,this);this.mon(this.el,'blur',this.onBlur,this,this.inEditor?{buffer:10}:null);},preFocus:Ext.emptyFn,onFocus:function(){this.preFocus();if(this.focusClass){this.el.addClass(this.focusClass);} +if(!this.hasFocus){this.hasFocus=true;this.startValue=this.getValue();this.fireEvent('focus',this);}},beforeBlur:Ext.emptyFn,onBlur:function(){this.beforeBlur();if(this.focusClass){this.el.removeClass(this.focusClass);} +this.hasFocus=false;if(this.validationEvent!==false&&(this.validateOnBlur||this.validationEvent=='blur')){this.validate();} +var v=this.getValue();if(String(v)!==String(this.startValue)){this.fireEvent('change',this,v,this.startValue);} +this.fireEvent('blur',this);this.postBlur();},postBlur:Ext.emptyFn,isValid:function(preventMark){if(this.disabled){return true;} +var restore=this.preventMark;this.preventMark=preventMark===true;var v=this.validateValue(this.processValue(this.getRawValue()));this.preventMark=restore;return v;},validate:function(){if(this.disabled||this.validateValue(this.processValue(this.getRawValue()))){this.clearInvalid();return true;} +return false;},processValue:function(value){return value;},validateValue:function(value){var error=this.getErrors(value)[0];if(error==undefined){return true;}else{this.markInvalid(error);return false;}},getErrors:function(){return[];},getActiveError:function(){return this.activeError||'';},markInvalid:function(msg){if(this.rendered&&!this.preventMark){msg=msg||this.invalidText;var mt=this.getMessageHandler();if(mt){mt.mark(this,msg);}else if(this.msgTarget){this.el.addClass(this.invalidClass);var t=Ext.getDom(this.msgTarget);if(t){t.innerHTML=msg;t.style.display=this.msgDisplay;}}} +this.setActiveError(msg);},clearInvalid:function(){if(this.rendered&&!this.preventMark){this.el.removeClass(this.invalidClass);var mt=this.getMessageHandler();if(mt){mt.clear(this);}else if(this.msgTarget){this.el.removeClass(this.invalidClass);var t=Ext.getDom(this.msgTarget);if(t){t.innerHTML='';t.style.display='none';}}} +this.unsetActiveError();},setActiveError:function(msg,suppressEvent){this.activeError=msg;if(suppressEvent!==true)this.fireEvent('invalid',this,msg);},unsetActiveError:function(suppressEvent){delete this.activeError;if(suppressEvent!==true)this.fireEvent('valid',this);},getMessageHandler:function(){return Ext.form.MessageTargets[this.msgTarget];},getErrorCt:function(){return this.el.findParent('.x-form-element',5,true)||this.el.findParent('.x-form-field-wrap',5,true);},alignErrorEl:function(){this.errorEl.setWidth(this.getErrorCt().getWidth(true)-20);},alignErrorIcon:function(){this.errorIcon.alignTo(this.el,'tl-tr',[2,0]);},getRawValue:function(){var v=this.rendered?this.el.getValue():Ext.value(this.value,'');if(v===this.emptyText){v='';} +return v;},getValue:function(){if(!this.rendered){return this.value;} +var v=this.el.getValue();if(v===this.emptyText||v===undefined){v='';} +return v;},setRawValue:function(v){return this.rendered?(this.el.dom.value=(Ext.isEmpty(v)?'':v)):'';},setValue:function(v){this.value=v;if(this.rendered){this.el.dom.value=(Ext.isEmpty(v)?'':v);this.validate();} +return this;},append:function(v){this.setValue([this.getValue(),v].join(''));}});Ext.form.MessageTargets={'qtip':{mark:function(field,msg){field.el.addClass(field.invalidClass);field.el.dom.qtip=msg;field.el.dom.qclass='x-form-invalid-tip';if(Ext.QuickTips){Ext.QuickTips.enable();}},clear:function(field){field.el.removeClass(field.invalidClass);field.el.dom.qtip='';}},'title':{mark:function(field,msg){field.el.addClass(field.invalidClass);field.el.dom.title=msg;},clear:function(field){field.el.dom.title='';}},'under':{mark:function(field,msg){field.el.addClass(field.invalidClass);if(!field.errorEl){var elp=field.getErrorCt();if(!elp){field.el.dom.title=msg;return;} +field.errorEl=elp.createChild({cls:'x-form-invalid-msg'});field.on('resize',field.alignErrorEl,field);field.on('destroy',function(){Ext.destroy(this.errorEl);},field);} +field.alignErrorEl();field.errorEl.update(msg);Ext.form.Field.msgFx[field.msgFx].show(field.errorEl,field);},clear:function(field){field.el.removeClass(field.invalidClass);if(field.errorEl){Ext.form.Field.msgFx[field.msgFx].hide(field.errorEl,field);}else{field.el.dom.title='';}}},'side':{mark:function(field,msg){field.el.addClass(field.invalidClass);if(!field.errorIcon){var elp=field.getErrorCt();if(!elp){field.el.dom.title=msg;return;} +field.errorIcon=elp.createChild({cls:'x-form-invalid-icon'});if(field.ownerCt){field.ownerCt.on('afterlayout',field.alignErrorIcon,field);field.ownerCt.on('expand',field.alignErrorIcon,field);} +field.on('resize',field.alignErrorIcon,field);field.on('destroy',function(){Ext.destroy(this.errorIcon);},field);} +field.alignErrorIcon();field.errorIcon.dom.qtip=msg;field.errorIcon.dom.qclass='x-form-invalid-tip';field.errorIcon.show();},clear:function(field){field.el.removeClass(field.invalidClass);if(field.errorIcon){field.errorIcon.dom.qtip='';field.errorIcon.hide();}else{field.el.dom.title='';}}}};Ext.form.Field.msgFx={normal:{show:function(msgEl,f){msgEl.setDisplayed('block');},hide:function(msgEl,f){msgEl.setDisplayed(false).update('');}},slide:{show:function(msgEl,f){msgEl.slideIn('t',{stopFx:true});},hide:function(msgEl,f){msgEl.slideOut('t',{stopFx:true,useDisplay:true});}},slideRight:{show:function(msgEl,f){msgEl.fixDisplay();msgEl.alignTo(f.el,'tl-tr');msgEl.slideIn('l',{stopFx:true});},hide:function(msgEl,f){msgEl.slideOut('l',{stopFx:true,useDisplay:true});}}};Ext.reg('field',Ext.form.Field);Ext.form.TextField=Ext.extend(Ext.form.Field,{grow:false,growMin:30,growMax:800,vtype:null,maskRe:null,disableKeyFilter:false,allowBlank:true,minLength:0,maxLength:Number.MAX_VALUE,minLengthText:'The minimum length for this field is {0}',maxLengthText:'The maximum length for this field is {0}',selectOnFocus:false,blankText:'This field is required',validator:null,regex:null,regexText:'',emptyText:null,emptyClass:'x-form-empty-field',initComponent:function(){Ext.form.TextField.superclass.initComponent.call(this);this.addEvents('autosize','keydown','keyup','keypress');},initEvents:function(){Ext.form.TextField.superclass.initEvents.call(this);if(this.validationEvent=='keyup'){this.validationTask=new Ext.util.DelayedTask(this.validate,this);this.mon(this.el,'keyup',this.filterValidation,this);} +else if(this.validationEvent!==false&&this.validationEvent!='blur'){this.mon(this.el,this.validationEvent,this.validate,this,{buffer:this.validationDelay});} +if(this.selectOnFocus||this.emptyText){this.mon(this.el,'mousedown',this.onMouseDown,this);if(this.emptyText){this.applyEmptyText();}} +if(this.maskRe||(this.vtype&&this.disableKeyFilter!==true&&(this.maskRe=Ext.form.VTypes[this.vtype+'Mask']))){this.mon(this.el,'keypress',this.filterKeys,this);} +if(this.grow){this.mon(this.el,'keyup',this.onKeyUpBuffered,this,{buffer:50});this.mon(this.el,'click',this.autoSize,this);} +if(this.enableKeyEvents){this.mon(this.el,{scope:this,keyup:this.onKeyUp,keydown:this.onKeyDown,keypress:this.onKeyPress});}},onMouseDown:function(e){if(!this.hasFocus){this.mon(this.el,'mouseup',Ext.emptyFn,this,{single:true,preventDefault:true});}},processValue:function(value){if(this.stripCharsRe){var newValue=value.replace(this.stripCharsRe,'');if(newValue!==value){this.setRawValue(newValue);return newValue;}} +return value;},filterValidation:function(e){if(!e.isNavKeyPress()){this.validationTask.delay(this.validationDelay);}},onDisable:function(){Ext.form.TextField.superclass.onDisable.call(this);if(Ext.isIE){this.el.dom.unselectable='on';}},onEnable:function(){Ext.form.TextField.superclass.onEnable.call(this);if(Ext.isIE){this.el.dom.unselectable='';}},onKeyUpBuffered:function(e){if(this.doAutoSize(e)){this.autoSize();}},doAutoSize:function(e){return!e.isNavKeyPress();},onKeyUp:function(e){this.fireEvent('keyup',this,e);},onKeyDown:function(e){this.fireEvent('keydown',this,e);},onKeyPress:function(e){this.fireEvent('keypress',this,e);},reset:function(){Ext.form.TextField.superclass.reset.call(this);this.applyEmptyText();},applyEmptyText:function(){if(this.rendered&&this.emptyText&&this.getRawValue().length<1&&!this.hasFocus){this.setRawValue(this.emptyText);this.el.addClass(this.emptyClass);}},preFocus:function(){var el=this.el,isEmpty;if(this.emptyText){if(el.dom.value==this.emptyText){this.setRawValue('');isEmpty=true;} +el.removeClass(this.emptyClass);} +if(this.selectOnFocus||isEmpty){el.dom.select();}},postBlur:function(){this.applyEmptyText();},filterKeys:function(e){if(e.ctrlKey){return;} +var k=e.getKey();if(Ext.isGecko&&(e.isNavKeyPress()||k==e.BACKSPACE||(k==e.DELETE&&e.button==-1))){return;} +var cc=String.fromCharCode(e.getCharCode());if(!Ext.isGecko&&e.isSpecialKey()&&!cc){return;} +if(!this.maskRe.test(cc)){e.stopEvent();}},setValue:function(v){if(this.emptyText&&this.el&&!Ext.isEmpty(v)){this.el.removeClass(this.emptyClass);} +Ext.form.TextField.superclass.setValue.apply(this,arguments);this.applyEmptyText();this.autoSize();return this;},getErrors:function(value){var errors=Ext.form.TextField.superclass.getErrors.apply(this,arguments);value=Ext.isDefined(value)?value:this.processValue(this.getRawValue());if(Ext.isFunction(this.validator)){var msg=this.validator(value);if(msg!==true){errors.push(msg);}} +if(value.length<1||value===this.emptyText){if(this.allowBlank){return errors;}else{errors.push(this.blankText);}} +if(!this.allowBlank&&(value.length<1||value===this.emptyText)){errors.push(this.blankText);} +if(value.length<this.minLength){errors.push(String.format(this.minLengthText,this.minLength));} +if(value.length>this.maxLength){errors.push(String.format(this.maxLengthText,this.maxLength));} +if(this.vtype){var vt=Ext.form.VTypes;if(!vt[this.vtype](value,this)){errors.push(this.vtypeText||vt[this.vtype+'Text']);}} +if(this.regex&&!this.regex.test(value)){errors.push(this.regexText);} +return errors;},selectText:function(start,end){var v=this.getRawValue();var doFocus=false;if(v.length>0){start=start===undefined?0:start;end=end===undefined?v.length:end;var d=this.el.dom;if(d.setSelectionRange){d.setSelectionRange(start,end);}else if(d.createTextRange){var range=d.createTextRange();range.moveStart('character',start);range.moveEnd('character',end-v.length);range.select();} +doFocus=Ext.isGecko||Ext.isOpera;}else{doFocus=true;} +if(doFocus){this.focus();}},autoSize:function(){if(!this.grow||!this.rendered){return;} +if(!this.metrics){this.metrics=Ext.util.TextMetrics.createInstance(this.el);} +var el=this.el;var v=el.dom.value;var d=document.createElement('div');d.appendChild(document.createTextNode(v));v=d.innerHTML;Ext.removeNode(d);d=null;v+=' ';var w=Math.min(this.growMax,Math.max(this.metrics.getWidth(v)+10,this.growMin));this.el.setWidth(w);this.fireEvent('autosize',this,w);},onDestroy:function(){if(this.validationTask){this.validationTask.cancel();this.validationTask=null;} +Ext.form.TextField.superclass.onDestroy.call(this);}});Ext.reg('textfield',Ext.form.TextField);Ext.form.TriggerField=Ext.extend(Ext.form.TextField,{defaultAutoCreate:{tag:"input",type:"text",size:"16",autocomplete:"off"},hideTrigger:false,editable:true,readOnly:false,wrapFocusClass:'x-trigger-wrap-focus',autoSize:Ext.emptyFn,monitorTab:true,deferHeight:true,mimicing:false,actionMode:'wrap',defaultTriggerWidth:17,onResize:function(w,h){Ext.form.TriggerField.superclass.onResize.call(this,w,h);var tw=this.getTriggerWidth();if(Ext.isNumber(w)){this.el.setWidth(w-tw);} +this.wrap.setWidth(this.el.getWidth()+tw);},getTriggerWidth:function(){var tw=this.trigger.getWidth();if(!this.hideTrigger&&!this.readOnly&&tw===0){tw=this.defaultTriggerWidth;} +return tw;},alignErrorIcon:function(){if(this.wrap){this.errorIcon.alignTo(this.wrap,'tl-tr',[2,0]);}},onRender:function(ct,position){this.doc=Ext.isIE?Ext.getBody():Ext.getDoc();Ext.form.TriggerField.superclass.onRender.call(this,ct,position);this.wrap=this.el.wrap({cls:'x-form-field-wrap x-form-field-trigger-wrap'});this.trigger=this.wrap.createChild(this.triggerConfig||{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.triggerClass});this.initTrigger();if(!this.width){this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth());} +this.resizeEl=this.positionEl=this.wrap;},getWidth:function(){return(this.el.getWidth()+this.trigger.getWidth());},updateEditState:function(){if(this.rendered){if(this.readOnly){this.el.dom.readOnly=true;this.el.addClass('x-trigger-noedit');this.mun(this.el,'click',this.onTriggerClick,this);this.trigger.setDisplayed(false);}else{if(!this.editable){this.el.dom.readOnly=true;this.el.addClass('x-trigger-noedit');this.mon(this.el,'click',this.onTriggerClick,this);}else{this.el.dom.readOnly=false;this.el.removeClass('x-trigger-noedit');this.mun(this.el,'click',this.onTriggerClick,this);} +this.trigger.setDisplayed(!this.hideTrigger);} +this.onResize(this.width||this.wrap.getWidth());}},setHideTrigger:function(hideTrigger){if(hideTrigger!=this.hideTrigger){this.hideTrigger=hideTrigger;this.updateEditState();}},setEditable:function(editable){if(editable!=this.editable){this.editable=editable;this.updateEditState();}},setReadOnly:function(readOnly){if(readOnly!=this.readOnly){this.readOnly=readOnly;this.updateEditState();}},afterRender:function(){Ext.form.TriggerField.superclass.afterRender.call(this);this.updateEditState();},initTrigger:function(){this.mon(this.trigger,'click',this.onTriggerClick,this,{preventDefault:true});this.trigger.addClassOnOver('x-form-trigger-over');this.trigger.addClassOnClick('x-form-trigger-click');},onDestroy:function(){Ext.destroy(this.trigger,this.wrap);if(this.mimicing){this.doc.un('mousedown',this.mimicBlur,this);} +delete this.doc;Ext.form.TriggerField.superclass.onDestroy.call(this);},onFocus:function(){Ext.form.TriggerField.superclass.onFocus.call(this);if(!this.mimicing){this.wrap.addClass(this.wrapFocusClass);this.mimicing=true;this.doc.on('mousedown',this.mimicBlur,this,{delay:10});if(this.monitorTab){this.on('specialkey',this.checkTab,this);}}},checkTab:function(me,e){if(e.getKey()==e.TAB){this.triggerBlur();}},onBlur:Ext.emptyFn,mimicBlur:function(e){if(!this.isDestroyed&&!this.wrap.contains(e.target)&&this.validateBlur(e)){this.triggerBlur();}},triggerBlur:function(){this.mimicing=false;this.doc.un('mousedown',this.mimicBlur,this);if(this.monitorTab&&this.el){this.un('specialkey',this.checkTab,this);} +Ext.form.TriggerField.superclass.onBlur.call(this);if(this.wrap){this.wrap.removeClass(this.wrapFocusClass);}},beforeBlur:Ext.emptyFn,validateBlur:function(e){return true;},onTriggerClick:Ext.emptyFn});Ext.form.TwinTriggerField=Ext.extend(Ext.form.TriggerField,{initComponent:function(){Ext.form.TwinTriggerField.superclass.initComponent.call(this);this.triggerConfig={tag:'span',cls:'x-form-twin-triggers',cn:[{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.trigger1Class},{tag:"img",src:Ext.BLANK_IMAGE_URL,alt:"",cls:"x-form-trigger "+this.trigger2Class}]};},getTrigger:function(index){return this.triggers[index];},afterRender:function(){Ext.form.TwinTriggerField.superclass.afterRender.call(this);var triggers=this.triggers,i=0,len=triggers.length;for(;i<len;++i){if(this['hideTrigger'+(i+1)]){triggers[i].hide();}}},initTrigger:function(){var ts=this.trigger.select('.x-form-trigger',true),triggerField=this;ts.each(function(t,all,index){var triggerIndex='Trigger'+(index+1);t.hide=function(){var w=triggerField.wrap.getWidth();this.dom.style.display='none';triggerField.el.setWidth(w-triggerField.trigger.getWidth());triggerField['hidden'+triggerIndex]=true;};t.show=function(){var w=triggerField.wrap.getWidth();this.dom.style.display='';triggerField.el.setWidth(w-triggerField.trigger.getWidth());triggerField['hidden'+triggerIndex]=false;};this.mon(t,'click',this['on'+triggerIndex+'Click'],this,{preventDefault:true});t.addClassOnOver('x-form-trigger-over');t.addClassOnClick('x-form-trigger-click');},this);this.triggers=ts.elements;},getTriggerWidth:function(){var tw=0;Ext.each(this.triggers,function(t,index){var triggerIndex='Trigger'+(index+1),w=t.getWidth();if(w===0&&!this['hidden'+triggerIndex]){tw+=this.defaultTriggerWidth;}else{tw+=w;}},this);return tw;},onDestroy:function(){Ext.destroy(this.triggers);Ext.form.TwinTriggerField.superclass.onDestroy.call(this);},onTrigger1Click:Ext.emptyFn,onTrigger2Click:Ext.emptyFn});Ext.reg('trigger',Ext.form.TriggerField);Ext.form.TextArea=Ext.extend(Ext.form.TextField,{growMin:60,growMax:1000,growAppend:' \n ',enterIsSpecial:false,preventScrollbars:false,onRender:function(ct,position){if(!this.el){this.defaultAutoCreate={tag:"textarea",style:"width:100px;height:60px;",autocomplete:"off"};} +Ext.form.TextArea.superclass.onRender.call(this,ct,position);if(this.grow){this.textSizeEl=Ext.DomHelper.append(document.body,{tag:"pre",cls:"x-form-grow-sizer"});if(this.preventScrollbars){this.el.setStyle("overflow","hidden");} +this.el.setHeight(this.growMin);}},onDestroy:function(){Ext.removeNode(this.textSizeEl);Ext.form.TextArea.superclass.onDestroy.call(this);},fireKey:function(e){if(e.isSpecialKey()&&(this.enterIsSpecial||(e.getKey()!=e.ENTER||e.hasModifier()))){this.fireEvent("specialkey",this,e);}},doAutoSize:function(e){return!e.isNavKeyPress()||e.getKey()==e.ENTER;},filterValidation:function(e){if(!e.isNavKeyPress()||(!this.enterIsSpecial&&e.keyCode==e.ENTER)){this.validationTask.delay(this.validationDelay);}},autoSize:function(){if(!this.grow||!this.textSizeEl){return;} +var el=this.el,v=Ext.util.Format.htmlEncode(el.dom.value),ts=this.textSizeEl,h;Ext.fly(ts).setWidth(this.el.getWidth());if(v.length<1){v="  ";}else{v+=this.growAppend;if(Ext.isIE){v=v.replace(/\n/g,' <br />');}} +ts.innerHTML=v;h=Math.min(this.growMax,Math.max(ts.offsetHeight,this.growMin));if(h!=this.lastHeight){this.lastHeight=h;this.el.setHeight(h);this.fireEvent("autosize",this,h);}}});Ext.reg('textarea',Ext.form.TextArea);Ext.form.NumberField=Ext.extend(Ext.form.TextField,{fieldClass:"x-form-field x-form-num-field",allowDecimals:true,decimalSeparator:".",decimalPrecision:2,allowNegative:true,minValue:Number.NEGATIVE_INFINITY,maxValue:Number.MAX_VALUE,minText:"The minimum value for this field is {0}",maxText:"The maximum value for this field is {0}",nanText:"{0} is not a valid number",baseChars:"0123456789",autoStripChars:false,initEvents:function(){var allowed=this.baseChars+'';if(this.allowDecimals){allowed+=this.decimalSeparator;} +if(this.allowNegative){allowed+='-';} +allowed=Ext.escapeRe(allowed);this.maskRe=new RegExp('['+allowed+']');if(this.autoStripChars){this.stripCharsRe=new RegExp('[^'+allowed+']','gi');} +Ext.form.NumberField.superclass.initEvents.call(this);},getErrors:function(value){var errors=Ext.form.NumberField.superclass.getErrors.apply(this,arguments);value=Ext.isDefined(value)?value:this.processValue(this.getRawValue());if(value.length<1){return errors;} +value=String(value).replace(this.decimalSeparator,".");if(isNaN(value)){errors.push(String.format(this.nanText,value));} +var num=this.parseValue(value);if(num<this.minValue){errors.push(String.format(this.minText,this.minValue));} +if(num>this.maxValue){errors.push(String.format(this.maxText,this.maxValue));} +return errors;},getValue:function(){return this.fixPrecision(this.parseValue(Ext.form.NumberField.superclass.getValue.call(this)));},setValue:function(v){v=this.fixPrecision(v);v=Ext.isNumber(v)?v:parseFloat(String(v).replace(this.decimalSeparator,"."));v=isNaN(v)?'':String(v).replace(".",this.decimalSeparator);return Ext.form.NumberField.superclass.setValue.call(this,v);},setMinValue:function(value){this.minValue=Ext.num(value,Number.NEGATIVE_INFINITY);},setMaxValue:function(value){this.maxValue=Ext.num(value,Number.MAX_VALUE);},parseValue:function(value){value=parseFloat(String(value).replace(this.decimalSeparator,"."));return isNaN(value)?'':value;},fixPrecision:function(value){var nan=isNaN(value);if(!this.allowDecimals||this.decimalPrecision==-1||nan||!value){return nan?'':value;} +return parseFloat(parseFloat(value).toFixed(this.decimalPrecision));},beforeBlur:function(){var v=this.parseValue(this.getRawValue());if(!Ext.isEmpty(v)){this.setValue(v);}}});Ext.reg('numberfield',Ext.form.NumberField);Ext.form.DateField=Ext.extend(Ext.form.TriggerField,{format:"m/d/Y",altFormats:"m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j",disabledDaysText:"Disabled",disabledDatesText:"Disabled",minText:"The date in this field must be equal to or after {0}",maxText:"The date in this field must be equal to or before {0}",invalidText:"{0} is not a valid date - it must be in the format {1}",triggerClass:'x-form-date-trigger',showToday:true,startDay:0,defaultAutoCreate:{tag:"input",type:"text",size:"10",autocomplete:"off"},initTime:'12',initTimeFormat:'H',safeParse:function(value,format){if(/[gGhH]/.test(format.replace(/(\\.)/g,''))){return Date.parseDate(value,format);}else{var parsedDate=Date.parseDate(value+' '+this.initTime,format+' '+this.initTimeFormat);if(parsedDate){return parsedDate.clearTime();}}},initComponent:function(){Ext.form.DateField.superclass.initComponent.call(this);this.addEvents('select');if(Ext.isString(this.minValue)){this.minValue=this.parseDate(this.minValue);} +if(Ext.isString(this.maxValue)){this.maxValue=this.parseDate(this.maxValue);} +this.disabledDatesRE=null;this.initDisabledDays();},initEvents:function(){Ext.form.DateField.superclass.initEvents.call(this);this.keyNav=new Ext.KeyNav(this.el,{"down":function(e){this.onTriggerClick();},scope:this,forceKeyDown:true});},initDisabledDays:function(){if(this.disabledDates){var dd=this.disabledDates,len=dd.length-1,re="(?:";Ext.each(dd,function(d,i){re+=Ext.isDate(d)?'^'+Ext.escapeRe(d.dateFormat(this.format))+'$':dd[i];if(i!=len){re+='|';}},this);this.disabledDatesRE=new RegExp(re+')');}},setDisabledDates:function(dd){this.disabledDates=dd;this.initDisabledDays();if(this.menu){this.menu.picker.setDisabledDates(this.disabledDatesRE);}},setDisabledDays:function(dd){this.disabledDays=dd;if(this.menu){this.menu.picker.setDisabledDays(dd);}},setMinValue:function(dt){this.minValue=(Ext.isString(dt)?this.parseDate(dt):dt);if(this.menu){this.menu.picker.setMinDate(this.minValue);}},setMaxValue:function(dt){this.maxValue=(Ext.isString(dt)?this.parseDate(dt):dt);if(this.menu){this.menu.picker.setMaxDate(this.maxValue);}},getErrors:function(value){var errors=Ext.form.DateField.superclass.getErrors.apply(this,arguments);value=this.formatDate(value||this.processValue(this.getRawValue()));if(value.length<1){return errors;} +var svalue=value;value=this.parseDate(value);if(!value){errors.push(String.format(this.invalidText,svalue,this.format));return errors;} +var time=value.getTime();if(this.minValue&&time<this.minValue.clearTime().getTime()){errors.push(String.format(this.minText,this.formatDate(this.minValue)));} +if(this.maxValue&&time>this.maxValue.clearTime().getTime()){errors.push(String.format(this.maxText,this.formatDate(this.maxValue)));} +if(this.disabledDays){var day=value.getDay();for(var i=0;i<this.disabledDays.length;i++){if(day===this.disabledDays[i]){errors.push(this.disabledDaysText);break;}}} +var fvalue=this.formatDate(value);if(this.disabledDatesRE&&this.disabledDatesRE.test(fvalue)){errors.push(String.format(this.disabledDatesText,fvalue));} +return errors;},validateBlur:function(){return!this.menu||!this.menu.isVisible();},getValue:function(){return this.parseDate(Ext.form.DateField.superclass.getValue.call(this))||"";},setValue:function(date){return Ext.form.DateField.superclass.setValue.call(this,this.formatDate(this.parseDate(date)));},parseDate:function(value){if(!value||Ext.isDate(value)){return value;} +var v=this.safeParse(value,this.format),af=this.altFormats,afa=this.altFormatsArray;if(!v&&af){afa=afa||af.split("|");for(var i=0,len=afa.length;i<len&&!v;i++){v=this.safeParse(value,afa[i]);}} +return v;},onDestroy:function(){Ext.destroy(this.menu,this.keyNav);Ext.form.DateField.superclass.onDestroy.call(this);},formatDate:function(date){return Ext.isDate(date)?date.dateFormat(this.format):date;},onTriggerClick:function(){if(this.disabled){return;} +if(this.menu==null){this.menu=new Ext.menu.DateMenu({hideOnClick:false,focusOnSelect:false});} +this.onFocus();Ext.apply(this.menu.picker,{minDate:this.minValue,maxDate:this.maxValue,disabledDatesRE:this.disabledDatesRE,disabledDatesText:this.disabledDatesText,disabledDays:this.disabledDays,disabledDaysText:this.disabledDaysText,format:this.format,showToday:this.showToday,startDay:this.startDay,minText:String.format(this.minText,this.formatDate(this.minValue)),maxText:String.format(this.maxText,this.formatDate(this.maxValue))});this.menu.picker.setValue(this.getValue()||new Date());this.menu.show(this.el,"tl-bl?");this.menuEvents('on');},menuEvents:function(method){this.menu[method]('select',this.onSelect,this);this.menu[method]('hide',this.onMenuHide,this);this.menu[method]('show',this.onFocus,this);},onSelect:function(m,d){this.setValue(d);this.fireEvent('select',this,d);this.menu.hide();},onMenuHide:function(){this.focus(false,60);this.menuEvents('un');},beforeBlur:function(){var v=this.parseDate(this.getRawValue());if(v){this.setValue(v);}}});Ext.reg('datefield',Ext.form.DateField);Ext.form.DisplayField=Ext.extend(Ext.form.Field,{validationEvent:false,validateOnBlur:false,defaultAutoCreate:{tag:"div"},fieldClass:"x-form-display-field",htmlEncode:false,initEvents:Ext.emptyFn,isValid:function(){return true;},validate:function(){return true;},getRawValue:function(){var v=this.rendered?this.el.dom.innerHTML:Ext.value(this.value,'');if(v===this.emptyText){v='';} +if(this.htmlEncode){v=Ext.util.Format.htmlDecode(v);} +return v;},getValue:function(){return this.getRawValue();},getName:function(){return this.name;},setRawValue:function(v){if(this.htmlEncode){v=Ext.util.Format.htmlEncode(v);} +return this.rendered?(this.el.dom.innerHTML=(Ext.isEmpty(v)?'':v)):(this.value=v);},setValue:function(v){this.setRawValue(v);return this;}});Ext.reg('displayfield',Ext.form.DisplayField);Ext.form.ComboBox=Ext.extend(Ext.form.TriggerField,{defaultAutoCreate:{tag:"input",type:"text",size:"24",autocomplete:"off"},listClass:'',selectedClass:'x-combo-selected',listEmptyText:'',triggerClass:'x-form-arrow-trigger',shadow:'sides',listAlign:'tl-bl?',maxHeight:300,minHeight:90,triggerAction:'query',minChars:4,autoSelect:true,typeAhead:false,queryDelay:500,pageSize:0,selectOnFocus:false,queryParam:'query',loadingText:'Loading...',resizable:false,handleHeight:8,allQuery:'',mode:'remote',minListWidth:70,forceSelection:false,typeAheadDelay:250,lazyInit:true,clearFilterOnReset:true,submitValue:undefined,initComponent:function(){Ext.form.ComboBox.superclass.initComponent.call(this);this.addEvents('expand','collapse','beforeselect','select','beforequery');if(this.transform){var s=Ext.getDom(this.transform);if(!this.hiddenName){this.hiddenName=s.name;} +if(!this.store){this.mode='local';var d=[],opts=s.options;for(var i=0,len=opts.length;i<len;i++){var o=opts[i],value=(o.hasAttribute?o.hasAttribute('value'):o.getAttributeNode('value').specified)?o.value:o.text;if(o.selected&&Ext.isEmpty(this.value,true)){this.value=value;} +d.push([value,o.text]);} +this.store=new Ext.data.ArrayStore({idIndex:0,fields:['value','text'],data:d,autoDestroy:true});this.valueField='value';this.displayField='text';} +s.name=Ext.id();if(!this.lazyRender){this.target=true;this.el=Ext.DomHelper.insertBefore(s,this.autoCreate||this.defaultAutoCreate);this.render(this.el.parentNode,s);} +Ext.removeNode(s);} +else if(this.store){this.store=Ext.StoreMgr.lookup(this.store);if(this.store.autoCreated){this.displayField=this.valueField='field1';if(!this.store.expandData){this.displayField='field2';} +this.mode='local';}} +this.selectedIndex=-1;if(this.mode=='local'){if(!Ext.isDefined(this.initialConfig.queryDelay)){this.queryDelay=10;} +if(!Ext.isDefined(this.initialConfig.minChars)){this.minChars=0;}}},onRender:function(ct,position){if(this.hiddenName&&!Ext.isDefined(this.submitValue)){this.submitValue=false;} +Ext.form.ComboBox.superclass.onRender.call(this,ct,position);if(this.hiddenName){this.hiddenField=this.el.insertSibling({tag:'input',type:'hidden',name:this.hiddenName,id:(this.hiddenId||Ext.id())},'before',true);} +if(Ext.isGecko){this.el.dom.setAttribute('autocomplete','off');} +if(!this.lazyInit){this.initList();}else{this.on('focus',this.initList,this,{single:true});}},initValue:function(){Ext.form.ComboBox.superclass.initValue.call(this);if(this.hiddenField){this.hiddenField.value=Ext.value(Ext.isDefined(this.hiddenValue)?this.hiddenValue:this.value,'');}},getParentZIndex:function(){var zindex;if(this.ownerCt){this.findParentBy(function(ct){zindex=parseInt(ct.getPositionEl().getStyle('z-index'),10);return!!zindex;});} +return zindex;},getZIndex:function(listParent){listParent=listParent||Ext.getDom(this.getListParent()||Ext.getBody());var zindex=parseInt(Ext.fly(listParent).getStyle('z-index'),10);if(!zindex){zindex=this.getParentZIndex();} +return(zindex||12000)+5;},initList:function(){if(!this.list){var cls='x-combo-list',listParent=Ext.getDom(this.getListParent()||Ext.getBody());this.list=new Ext.Layer({parentEl:listParent,shadow:this.shadow,cls:[cls,this.listClass].join(' '),constrain:false,zindex:this.getZIndex(listParent)});var lw=this.listWidth||Math.max(this.wrap.getWidth(),this.minListWidth);this.list.setSize(lw,0);this.list.swallowEvent('mousewheel');this.assetHeight=0;if(this.syncFont!==false){this.list.setStyle('font-size',this.el.getStyle('font-size'));} +if(this.title){this.header=this.list.createChild({cls:cls+'-hd',html:this.title});this.assetHeight+=this.header.getHeight();} +this.innerList=this.list.createChild({cls:cls+'-inner'});this.mon(this.innerList,'mouseover',this.onViewOver,this);this.mon(this.innerList,'mousemove',this.onViewMove,this);this.innerList.setWidth(lw-this.list.getFrameWidth('lr'));if(this.pageSize){this.footer=this.list.createChild({cls:cls+'-ft'});this.pageTb=new Ext.PagingToolbar({store:this.store,pageSize:this.pageSize,renderTo:this.footer});this.assetHeight+=this.footer.getHeight();} +if(!this.tpl){this.tpl='<tpl for="."><div class="'+cls+'-item">{'+this.displayField+'}</div></tpl>';} +this.view=new Ext.DataView({applyTo:this.innerList,tpl:this.tpl,singleSelect:true,selectedClass:this.selectedClass,itemSelector:this.itemSelector||'.'+cls+'-item',emptyText:this.listEmptyText,deferEmptyText:false});this.mon(this.view,{containerclick:this.onViewClick,click:this.onViewClick,scope:this});this.bindStore(this.store,true);if(this.resizable){this.resizer=new Ext.Resizable(this.list,{pinned:true,handles:'se'});this.mon(this.resizer,'resize',function(r,w,h){this.maxHeight=h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;this.listWidth=w;this.innerList.setWidth(w-this.list.getFrameWidth('lr'));this.restrictHeight();},this);this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom',this.handleHeight+'px');}}},getListParent:function(){return document.body;},getStore:function(){return this.store;},bindStore:function(store,initial){if(this.store&&!initial){if(this.store!==store&&this.store.autoDestroy){this.store.destroy();}else{this.store.un('beforeload',this.onBeforeLoad,this);this.store.un('load',this.onLoad,this);this.store.un('exception',this.collapse,this);} +if(!store){this.store=null;if(this.view){this.view.bindStore(null);} +if(this.pageTb){this.pageTb.bindStore(null);}}} +if(store){if(!initial){this.lastQuery=null;if(this.pageTb){this.pageTb.bindStore(store);}} +this.store=Ext.StoreMgr.lookup(store);this.store.on({scope:this,beforeload:this.onBeforeLoad,load:this.onLoad,exception:this.collapse});if(this.view){this.view.bindStore(store);}}},reset:function(){if(this.clearFilterOnReset&&this.mode=='local'){this.store.clearFilter();} +Ext.form.ComboBox.superclass.reset.call(this);},initEvents:function(){Ext.form.ComboBox.superclass.initEvents.call(this);this.keyNav=new Ext.KeyNav(this.el,{"up":function(e){this.inKeyMode=true;this.selectPrev();},"down":function(e){if(!this.isExpanded()){this.onTriggerClick();}else{this.inKeyMode=true;this.selectNext();}},"enter":function(e){this.onViewClick();},"esc":function(e){this.collapse();},"tab":function(e){if(this.forceSelection===true){this.collapse();}else{this.onViewClick(false);} +return true;},scope:this,doRelay:function(e,h,hname){if(hname=='down'||this.scope.isExpanded()){var relay=Ext.KeyNav.prototype.doRelay.apply(this,arguments);if(!Ext.isIE&&Ext.EventManager.useKeydown){this.scope.fireKey(e);} +return relay;} +return true;},forceKeyDown:true,defaultEventAction:'stopEvent'});this.queryDelay=Math.max(this.queryDelay||10,this.mode=='local'?10:250);this.dqTask=new Ext.util.DelayedTask(this.initQuery,this);if(this.typeAhead){this.taTask=new Ext.util.DelayedTask(this.onTypeAhead,this);} +if(!this.enableKeyEvents){this.mon(this.el,'keyup',this.onKeyUp,this);}},onDestroy:function(){if(this.dqTask){this.dqTask.cancel();this.dqTask=null;} +this.bindStore(null);Ext.destroy(this.resizer,this.view,this.pageTb,this.list);Ext.destroyMembers(this,'hiddenField');Ext.form.ComboBox.superclass.onDestroy.call(this);},fireKey:function(e){if(!this.isExpanded()){Ext.form.ComboBox.superclass.fireKey.call(this,e);}},onResize:function(w,h){Ext.form.ComboBox.superclass.onResize.apply(this,arguments);if(!isNaN(w)&&this.isVisible()&&this.list){this.doResize(w);}else{this.bufferSize=w;}},doResize:function(w){if(!Ext.isDefined(this.listWidth)){var lw=Math.max(w,this.minListWidth);this.list.setWidth(lw);this.innerList.setWidth(lw-this.list.getFrameWidth('lr'));}},onEnable:function(){Ext.form.ComboBox.superclass.onEnable.apply(this,arguments);if(this.hiddenField){this.hiddenField.disabled=false;}},onDisable:function(){Ext.form.ComboBox.superclass.onDisable.apply(this,arguments);if(this.hiddenField){this.hiddenField.disabled=true;}},onBeforeLoad:function(){if(!this.hasFocus){return;} +this.innerList.update(this.loadingText?'<div class="loading-indicator">'+this.loadingText+'</div>':'');this.restrictHeight();this.selectedIndex=-1;},onLoad:function(){if(!this.hasFocus){return;} +if(this.store.getCount()>0||this.listEmptyText){this.expand();this.restrictHeight();if(this.lastQuery==this.allQuery){if(this.editable){this.el.dom.select();} +if(this.autoSelect!==false&&!this.selectByValue(this.value,true)){this.select(0,true);}}else{if(this.autoSelect!==false){this.selectNext();} +if(this.typeAhead&&this.lastKey!=Ext.EventObject.BACKSPACE&&this.lastKey!=Ext.EventObject.DELETE){this.taTask.delay(this.typeAheadDelay);}}}else{this.collapse();}},onTypeAhead:function(){if(this.store.getCount()>0){var r=this.store.getAt(0);var newValue=r.data[this.displayField];var len=newValue.length;var selStart=this.getRawValue().length;if(selStart!=len){this.setRawValue(newValue);this.selectText(selStart,newValue.length);}}},assertValue:function(){var val=this.getRawValue(),rec;if(this.valueField&&Ext.isDefined(this.value)){rec=this.findRecord(this.valueField,this.value);} +if(!rec||rec.get(this.displayField)!=val){rec=this.findRecord(this.displayField,val);} +if(!rec&&this.forceSelection){if(val.length>0&&val!=this.emptyText){this.el.dom.value=Ext.value(this.lastSelectionText,'');this.applyEmptyText();}else{this.clearValue();}}else{if(rec&&this.valueField){if(this.value==val){return;} +val=rec.get(this.valueField||this.displayField);} +this.setValue(val);}},onSelect:function(record,index){if(this.fireEvent('beforeselect',this,record,index)!==false){this.setValue(record.data[this.valueField||this.displayField]);this.collapse();this.fireEvent('select',this,record,index);}},getName:function(){var hf=this.hiddenField;return hf&&hf.name?hf.name:this.hiddenName||Ext.form.ComboBox.superclass.getName.call(this);},getValue:function(){if(this.valueField){return Ext.isDefined(this.value)?this.value:'';}else{return Ext.form.ComboBox.superclass.getValue.call(this);}},clearValue:function(){if(this.hiddenField){this.hiddenField.value='';} +this.setRawValue('');this.lastSelectionText='';this.applyEmptyText();this.value='';},setValue:function(v){var text=v;if(this.valueField){var r=this.findRecord(this.valueField,v);if(r){text=r.data[this.displayField];}else if(Ext.isDefined(this.valueNotFoundText)){text=this.valueNotFoundText;}} +this.lastSelectionText=text;if(this.hiddenField){this.hiddenField.value=Ext.value(v,'');} +Ext.form.ComboBox.superclass.setValue.call(this,text);this.value=v;return this;},findRecord:function(prop,value){var record;if(this.store.getCount()>0){this.store.each(function(r){if(r.data[prop]==value){record=r;return false;}});} +return record;},onViewMove:function(e,t){this.inKeyMode=false;},onViewOver:function(e,t){if(this.inKeyMode){return;} +var item=this.view.findItemFromChild(t);if(item){var index=this.view.indexOf(item);this.select(index,false);}},onViewClick:function(doFocus){var index=this.view.getSelectedIndexes()[0],s=this.store,r=s.getAt(index);if(r){this.onSelect(r,index);}else{this.collapse();} +if(doFocus!==false){this.el.focus();}},restrictHeight:function(){this.innerList.dom.style.height='';var inner=this.innerList.dom,pad=this.list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight,h=Math.max(inner.clientHeight,inner.offsetHeight,inner.scrollHeight),ha=this.getPosition()[1]-Ext.getBody().getScroll().top,hb=Ext.lib.Dom.getViewHeight()-ha-this.getSize().height,space=Math.max(ha,hb,this.minHeight||0)-this.list.shadowOffset-pad-5;h=Math.min(h,space,this.maxHeight);this.innerList.setHeight(h);this.list.beginUpdate();this.list.setHeight(h+pad);this.list.alignTo.apply(this.list,[this.el].concat(this.listAlign));this.list.endUpdate();},isExpanded:function(){return this.list&&this.list.isVisible();},selectByValue:function(v,scrollIntoView){if(!Ext.isEmpty(v,true)){var r=this.findRecord(this.valueField||this.displayField,v);if(r){this.select(this.store.indexOf(r),scrollIntoView);return true;}} +return false;},select:function(index,scrollIntoView){this.selectedIndex=index;this.view.select(index);if(scrollIntoView!==false){var el=this.view.getNode(index);if(el){this.innerList.scrollChildIntoView(el,false);}}},selectNext:function(){var ct=this.store.getCount();if(ct>0){if(this.selectedIndex==-1){this.select(0);}else if(this.selectedIndex<ct-1){this.select(this.selectedIndex+1);}}},selectPrev:function(){var ct=this.store.getCount();if(ct>0){if(this.selectedIndex==-1){this.select(0);}else if(this.selectedIndex!==0){this.select(this.selectedIndex-1);}}},onKeyUp:function(e){var k=e.getKey();if(this.editable!==false&&this.readOnly!==true&&(k==e.BACKSPACE||!e.isSpecialKey())){this.lastKey=k;this.dqTask.delay(this.queryDelay);} +Ext.form.ComboBox.superclass.onKeyUp.call(this,e);},validateBlur:function(){return!this.list||!this.list.isVisible();},initQuery:function(){this.doQuery(this.getRawValue());},beforeBlur:function(){this.assertValue();},postBlur:function(){Ext.form.ComboBox.superclass.postBlur.call(this);this.collapse();this.inKeyMode=false;},doQuery:function(q,forceAll){q=Ext.isEmpty(q)?'':q;var qe={query:q,forceAll:forceAll,combo:this,cancel:false};if(this.fireEvent('beforequery',qe)===false||qe.cancel){return false;} +q=qe.query;forceAll=qe.forceAll;if(forceAll===true||(q.length>=this.minChars)){if(this.lastQuery!==q){this.lastQuery=q;if(this.mode=='local'){this.selectedIndex=-1;if(forceAll){this.store.clearFilter();}else{this.store.filter(this.displayField,q);} +this.onLoad();}else{this.store.baseParams[this.queryParam]=q;this.store.load({params:this.getParams(q)});this.expand();}}else{this.selectedIndex=-1;this.onLoad();}}},getParams:function(q){var params={},paramNames=this.store.paramNames;if(this.pageSize){params[paramNames.start]=0;params[paramNames.limit]=this.pageSize;} +return params;},collapse:function(){if(!this.isExpanded()){return;} +this.list.hide();Ext.getDoc().un('mousewheel',this.collapseIf,this);Ext.getDoc().un('mousedown',this.collapseIf,this);this.fireEvent('collapse',this);},collapseIf:function(e){if(!this.isDestroyed&&!e.within(this.wrap)&&!e.within(this.list)){this.collapse();}},expand:function(){if(this.isExpanded()||!this.hasFocus){return;} +if(this.title||this.pageSize){this.assetHeight=0;if(this.title){this.assetHeight+=this.header.getHeight();} +if(this.pageSize){this.assetHeight+=this.footer.getHeight();}} +if(this.bufferSize){this.doResize(this.bufferSize);delete this.bufferSize;} +this.list.alignTo.apply(this.list,[this.el].concat(this.listAlign));this.list.setZIndex(this.getZIndex());this.list.show();if(Ext.isGecko2){this.innerList.setOverflow('auto');} +this.mon(Ext.getDoc(),{scope:this,mousewheel:this.collapseIf,mousedown:this.collapseIf});this.fireEvent('expand',this);},onTriggerClick:function(){if(this.readOnly||this.disabled){return;} +if(this.isExpanded()){this.collapse();this.el.focus();}else{this.onFocus({});if(this.triggerAction=='all'){this.doQuery(this.allQuery,true);}else{this.doQuery(this.getRawValue());} +this.el.focus();}}});Ext.reg('combo',Ext.form.ComboBox);Ext.form.Checkbox=Ext.extend(Ext.form.Field,{focusClass:undefined,fieldClass:'x-form-field',checked:false,boxLabel:' ',defaultAutoCreate:{tag:'input',type:'checkbox',autocomplete:'off'},actionMode:'wrap',initComponent:function(){Ext.form.Checkbox.superclass.initComponent.call(this);this.addEvents('check');},onResize:function(){Ext.form.Checkbox.superclass.onResize.apply(this,arguments);if(!this.boxLabel&&!this.fieldLabel){this.el.alignTo(this.wrap,'c-c');}},initEvents:function(){Ext.form.Checkbox.superclass.initEvents.call(this);this.mon(this.el,{scope:this,click:this.onClick,change:this.onClick});},markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn,onRender:function(ct,position){Ext.form.Checkbox.superclass.onRender.call(this,ct,position);if(this.inputValue!==undefined){this.el.dom.value=this.inputValue;} +this.wrap=this.el.wrap({cls:'x-form-check-wrap'});if(this.boxLabel){this.wrap.createChild({tag:'label',htmlFor:this.el.id,cls:'x-form-cb-label',html:this.boxLabel});} +if(this.checked){this.setValue(true);}else{this.checked=this.el.dom.checked;} +if(Ext.isIE&&!Ext.isStrict){this.wrap.repaint();} +this.resizeEl=this.positionEl=this.wrap;},onDestroy:function(){Ext.destroy(this.wrap);Ext.form.Checkbox.superclass.onDestroy.call(this);},initValue:function(){this.originalValue=this.getValue();},getValue:function(){if(this.rendered){return this.el.dom.checked;} +return this.checked;},onClick:function(){if(this.el.dom.checked!=this.checked){this.setValue(this.el.dom.checked);}},setValue:function(v){var checked=this.checked,inputVal=this.inputValue;this.checked=(v===true||v==='true'||v=='1'||(inputVal?v==inputVal:String(v).toLowerCase()=='on'));if(this.rendered){this.el.dom.checked=this.checked;this.el.dom.defaultChecked=this.checked;} +if(checked!=this.checked){this.fireEvent('check',this,this.checked);if(this.handler){this.handler.call(this.scope||this,this,this.checked);}} +return this;}});Ext.reg('checkbox',Ext.form.Checkbox);Ext.form.CheckboxGroup=Ext.extend(Ext.form.Field,{columns:'auto',vertical:false,allowBlank:true,blankText:"You must select at least one item in this group",defaultType:'checkbox',groupCls:'x-form-check-group',initComponent:function(){this.addEvents('change');this.on('change',this.validate,this);Ext.form.CheckboxGroup.superclass.initComponent.call(this);},onRender:function(ct,position){if(!this.el){var panelCfg={autoEl:{id:this.id},cls:this.groupCls,layout:'column',renderTo:ct,bufferResize:false};var colCfg={xtype:'container',defaultType:this.defaultType,layout:'form',defaults:{hideLabel:true,anchor:'100%'}};if(this.items[0].items){Ext.apply(panelCfg,{layoutConfig:{columns:this.items.length},defaults:this.defaults,items:this.items});for(var i=0,len=this.items.length;i<len;i++){Ext.applyIf(this.items[i],colCfg);}}else{var numCols,cols=[];if(typeof this.columns=='string'){this.columns=this.items.length;} +if(!Ext.isArray(this.columns)){var cs=[];for(var i=0;i<this.columns;i++){cs.push((100/this.columns)*.01);} +this.columns=cs;} +numCols=this.columns.length;for(var i=0;i<numCols;i++){var cc=Ext.apply({items:[]},colCfg);cc[this.columns[i]<=1?'columnWidth':'width']=this.columns[i];if(this.defaults){cc.defaults=Ext.apply(cc.defaults||{},this.defaults);} +cols.push(cc);};if(this.vertical){var rows=Math.ceil(this.items.length/numCols),ri=0;for(var i=0,len=this.items.length;i<len;i++){if(i>0&&i%rows==0){ri++;} +if(this.items[i].fieldLabel){this.items[i].hideLabel=false;} +cols[ri].items.push(this.items[i]);};}else{for(var i=0,len=this.items.length;i<len;i++){var ci=i%numCols;if(this.items[i].fieldLabel){this.items[i].hideLabel=false;} +cols[ci].items.push(this.items[i]);};} +Ext.apply(panelCfg,{layoutConfig:{columns:numCols},items:cols});} +this.panel=new Ext.Container(panelCfg);this.panel.ownerCt=this;this.el=this.panel.getEl();if(this.forId&&this.itemCls){var l=this.el.up(this.itemCls).child('label',true);if(l){l.setAttribute('htmlFor',this.forId);}} +var fields=this.panel.findBy(function(c){return c.isFormField;},this);this.items=new Ext.util.MixedCollection();this.items.addAll(fields);} +Ext.form.CheckboxGroup.superclass.onRender.call(this,ct,position);},initValue:function(){if(this.value){this.setValue.apply(this,this.buffered?this.value:[this.value]);delete this.buffered;delete this.value;}},afterRender:function(){Ext.form.CheckboxGroup.superclass.afterRender.call(this);this.eachItem(function(item){item.on('check',this.fireChecked,this);item.inGroup=true;});},doLayout:function(){if(this.rendered){this.panel.forceLayout=this.ownerCt.forceLayout;this.panel.doLayout();}},fireChecked:function(){var arr=[];this.eachItem(function(item){if(item.checked){arr.push(item);}});this.fireEvent('change',this,arr);},getErrors:function(){var errors=Ext.form.CheckboxGroup.superclass.getErrors.apply(this,arguments);if(!this.allowBlank){var blank=true;this.eachItem(function(f){if(f.checked){return(blank=false);}});if(blank)errors.push(this.blankText);} +return errors;},isDirty:function(){if(this.disabled||!this.rendered){return false;} +var dirty=false;this.eachItem(function(item){if(item.isDirty()){dirty=true;return false;}});return dirty;},setReadOnly:function(readOnly){if(this.rendered){this.eachItem(function(item){item.setReadOnly(readOnly);});} +this.readOnly=readOnly;},onDisable:function(){this.eachItem(function(item){item.disable();});},onEnable:function(){this.eachItem(function(item){item.enable();});},onResize:function(w,h){this.panel.setSize(w,h);this.panel.doLayout();},reset:function(){if(this.originalValue){this.eachItem(function(c){if(c.setValue){c.setValue(false);c.originalValue=c.getValue();}});this.resetOriginal=true;this.setValue(this.originalValue);delete this.resetOriginal;}else{this.eachItem(function(c){if(c.reset){c.reset();}});} +(function(){this.clearInvalid();}).defer(50,this);},setValue:function(){if(this.rendered){this.onSetValue.apply(this,arguments);}else{this.buffered=true;this.value=arguments;} +return this;},onSetValue:function(id,value){if(arguments.length==1){if(Ext.isArray(id)){Ext.each(id,function(val,idx){if(Ext.isObject(val)&&val.setValue){val.setValue(true);if(this.resetOriginal===true){val.originalValue=val.getValue();}}else{var item=this.items.itemAt(idx);if(item){item.setValue(val);}}},this);}else if(Ext.isObject(id)){for(var i in id){var f=this.getBox(i);if(f){f.setValue(id[i]);}}}else{this.setValueForItem(id);}}else{var f=this.getBox(id);if(f){f.setValue(value);}}},beforeDestroy:function(){Ext.destroy(this.panel);if(!this.rendered){Ext.destroy(this.items);} +Ext.form.CheckboxGroup.superclass.beforeDestroy.call(this);},setValueForItem:function(val){val=String(val).split(',');this.eachItem(function(item){if(val.indexOf(item.inputValue)>-1){item.setValue(true);}});},getBox:function(id){var box=null;this.eachItem(function(f){if(id==f||f.dataIndex==id||f.id==id||f.getName()==id){box=f;return false;}});return box;},getValue:function(){var out=[];this.eachItem(function(item){if(item.checked){out.push(item);}});return out;},eachItem:function(fn,scope){if(this.items&&this.items.each){this.items.each(fn,scope||this);}},getRawValue:Ext.emptyFn,setRawValue:Ext.emptyFn});Ext.reg('checkboxgroup',Ext.form.CheckboxGroup);Ext.form.CompositeField=Ext.extend(Ext.form.Field,{defaultMargins:'0 5 0 0',skipLastItemMargin:true,isComposite:true,combineErrors:true,labelConnector:', ',initComponent:function(){var labels=[],items=this.items,item;for(var i=0,j=items.length;i<j;i++){item=items[i];if(!Ext.isEmpty(item.ref)){item.ref='../'+item.ref;} +labels.push(item.fieldLabel);Ext.applyIf(item,this.defaults);if(!(i==j-1&&this.skipLastItemMargin)){Ext.applyIf(item,{margins:this.defaultMargins});}} +this.fieldLabel=this.fieldLabel||this.buildLabel(labels);this.fieldErrors=new Ext.util.MixedCollection(true,function(item){return item.field;});this.fieldErrors.on({scope:this,add:this.updateInvalidMark,remove:this.updateInvalidMark,replace:this.updateInvalidMark});Ext.form.CompositeField.superclass.initComponent.apply(this,arguments);this.innerCt=new Ext.Container({layout:'hbox',items:this.items,cls:'x-form-composite',defaultMargins:'0 3 0 0',ownerCt:this});this.innerCt.ownerCt=undefined;var fields=this.innerCt.findBy(function(c){return c.isFormField;},this);this.items=new Ext.util.MixedCollection();this.items.addAll(fields);},onRender:function(ct,position){if(!this.el){var innerCt=this.innerCt;innerCt.render(ct);this.el=innerCt.getEl();if(this.combineErrors){this.eachItem(function(field){Ext.apply(field,{markInvalid:this.onFieldMarkInvalid.createDelegate(this,[field],0),clearInvalid:this.onFieldClearInvalid.createDelegate(this,[field],0)});});} +var l=this.el.parent().parent().child('label',true);if(l){l.setAttribute('for',this.items.items[0].id);}} +Ext.form.CompositeField.superclass.onRender.apply(this,arguments);},onFieldMarkInvalid:function(field,message){var name=field.getName(),error={field:name,errorName:field.fieldLabel||name,error:message};this.fieldErrors.replace(name,error);field.el.addClass(field.invalidClass);},onFieldClearInvalid:function(field){this.fieldErrors.removeKey(field.getName());field.el.removeClass(field.invalidClass);},updateInvalidMark:function(){var ieStrict=Ext.isIE6&&Ext.isStrict;if(this.fieldErrors.length==0){this.clearInvalid();if(ieStrict){this.clearInvalid.defer(50,this);}}else{var message=this.buildCombinedErrorMessage(this.fieldErrors.items);this.sortErrors();this.markInvalid(message);if(ieStrict){this.markInvalid(message);}}},validateValue:function(){var valid=true;this.eachItem(function(field){if(!field.isValid())valid=false;});return valid;},buildCombinedErrorMessage:function(errors){var combined=[],error;for(var i=0,j=errors.length;i<j;i++){error=errors[i];combined.push(String.format("{0}: {1}",error.errorName,error.error));} +return combined.join("<br />");},sortErrors:function(){var fields=this.items;this.fieldErrors.sort("ASC",function(a,b){var findByName=function(key){return function(field){return field.getName()==key;};};var aIndex=fields.findIndexBy(findByName(a.field)),bIndex=fields.findIndexBy(findByName(b.field));return aIndex<bIndex?-1:1;});},reset:function(){this.eachItem(function(item){item.reset();});(function(){this.clearInvalid();}).defer(50,this);},clearInvalidChildren:function(){this.eachItem(function(item){item.clearInvalid();});},buildLabel:function(segments){return Ext.clean(segments).join(this.labelConnector);},isDirty:function(){if(this.disabled||!this.rendered){return false;} +var dirty=false;this.eachItem(function(item){if(item.isDirty()){dirty=true;return false;}});return dirty;},eachItem:function(fn,scope){if(this.items&&this.items.each){this.items.each(fn,scope||this);}},onResize:function(adjWidth,adjHeight,rawWidth,rawHeight){var innerCt=this.innerCt;if(this.rendered&&innerCt.rendered){innerCt.setSize(adjWidth,adjHeight);} +Ext.form.CompositeField.superclass.onResize.apply(this,arguments);},doLayout:function(shallow,force){if(this.rendered){var innerCt=this.innerCt;innerCt.forceLayout=this.ownerCt.forceLayout;innerCt.doLayout(shallow,force);}},beforeDestroy:function(){Ext.destroy(this.innerCt);Ext.form.CompositeField.superclass.beforeDestroy.call(this);},setReadOnly:function(readOnly){if(readOnly==undefined){readOnly=true;} +readOnly=!!readOnly;if(this.rendered){this.eachItem(function(item){item.setReadOnly(readOnly);});} +this.readOnly=readOnly;},onShow:function(){Ext.form.CompositeField.superclass.onShow.call(this);this.doLayout();},onDisable:function(){this.eachItem(function(item){item.disable();});},onEnable:function(){this.eachItem(function(item){item.enable();});}});Ext.reg('compositefield',Ext.form.CompositeField);Ext.form.Radio=Ext.extend(Ext.form.Checkbox,{inputType:'radio',markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn,getGroupValue:function(){var p=this.el.up('form')||Ext.getBody();var c=p.child('input[name='+this.el.dom.name+']:checked',true);return c?c.value:null;},setValue:function(v){var checkEl,els,radio;if(typeof v=='boolean'){Ext.form.Radio.superclass.setValue.call(this,v);}else if(this.rendered){checkEl=this.getCheckEl();radio=checkEl.child('input[name='+this.el.dom.name+'][value='+v+']',true);if(radio){Ext.getCmp(radio.id).setValue(true);}} +if(this.rendered&&this.checked){checkEl=checkEl||this.getCheckEl();els=this.getCheckEl().select('input[name='+this.el.dom.name+']');els.each(function(el){if(el.dom.id!=this.id){Ext.getCmp(el.dom.id).setValue(false);}},this);} +return this;},getCheckEl:function(){if(this.inGroup){return this.el.up('.x-form-radio-group');} +return this.el.up('form')||Ext.getBody();}});Ext.reg('radio',Ext.form.Radio);Ext.form.RadioGroup=Ext.extend(Ext.form.CheckboxGroup,{allowBlank:true,blankText:'You must select one item in this group',defaultType:'radio',groupCls:'x-form-radio-group',getValue:function(){var out=null;this.eachItem(function(item){if(item.checked){out=item;return false;}});return out;},onSetValue:function(id,value){if(arguments.length>1){var f=this.getBox(id);if(f){f.setValue(value);if(f.checked){this.eachItem(function(item){if(item!==f){item.setValue(false);}});}}}else{this.setValueForItem(id);}},setValueForItem:function(val){val=String(val).split(',')[0];this.eachItem(function(item){item.setValue(val==item.inputValue);});},fireChecked:function(){if(!this.checkTask){this.checkTask=new Ext.util.DelayedTask(this.bufferChecked,this);} +this.checkTask.delay(10);},bufferChecked:function(){var out=null;this.eachItem(function(item){if(item.checked){out=item;return false;}});this.fireEvent('change',this,out);},onDestroy:function(){if(this.checkTask){this.checkTask.cancel();this.checkTask=null;} +Ext.form.RadioGroup.superclass.onDestroy.call(this);}});Ext.reg('radiogroup',Ext.form.RadioGroup);Ext.form.Hidden=Ext.extend(Ext.form.Field,{inputType:'hidden',shouldLayout:false,onRender:function(){Ext.form.Hidden.superclass.onRender.apply(this,arguments);},initEvents:function(){this.originalValue=this.getValue();},setSize:Ext.emptyFn,setWidth:Ext.emptyFn,setHeight:Ext.emptyFn,setPosition:Ext.emptyFn,setPagePosition:Ext.emptyFn,markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn});Ext.reg('hidden',Ext.form.Hidden);Ext.form.BasicForm=Ext.extend(Ext.util.Observable,{constructor:function(el,config){Ext.apply(this,config);if(Ext.isString(this.paramOrder)){this.paramOrder=this.paramOrder.split(/[\s,|]/);} +this.items=new Ext.util.MixedCollection(false,function(o){return o.getItemId();});this.addEvents('beforeaction','actionfailed','actioncomplete');if(el){this.initEl(el);} +Ext.form.BasicForm.superclass.constructor.call(this);},timeout:30,paramOrder:undefined,paramsAsHash:false,waitTitle:'Please Wait...',activeAction:null,trackResetOnLoad:false,initEl:function(el){this.el=Ext.get(el);this.id=this.el.id||Ext.id();if(!this.standardSubmit){this.el.on('submit',this.onSubmit,this);} +this.el.addClass('x-form');},getEl:function(){return this.el;},onSubmit:function(e){e.stopEvent();},destroy:function(bound){if(bound!==true){this.items.each(function(f){Ext.destroy(f);});Ext.destroy(this.el);} +this.items.clear();this.purgeListeners();},isValid:function(){var valid=true;this.items.each(function(f){if(!f.validate()){valid=false;}});return valid;},isDirty:function(){var dirty=false;this.items.each(function(f){if(f.isDirty()){dirty=true;return false;}});return dirty;},doAction:function(action,options){if(Ext.isString(action)){action=new Ext.form.Action.ACTION_TYPES[action](this,options);} +if(this.fireEvent('beforeaction',this,action)!==false){this.beforeAction(action);action.run.defer(100,action);} +return this;},submit:function(options){options=options||{};if(this.standardSubmit){var v=options.clientValidation===false||this.isValid();if(v){var el=this.el.dom;if(this.url&&Ext.isEmpty(el.action)){el.action=this.url;} +el.submit();} +return v;} +var submitAction=String.format('{0}submit',this.api?'direct':'');this.doAction(submitAction,options);return this;},load:function(options){var loadAction=String.format('{0}load',this.api?'direct':'');this.doAction(loadAction,options);return this;},updateRecord:function(record){record.beginEdit();var fs=record.fields,field,value;fs.each(function(f){field=this.findField(f.name);if(field){value=field.getValue();if(typeof value!=undefined&&value.getGroupValue){value=value.getGroupValue();}else if(field.eachItem){value=[];field.eachItem(function(item){value.push(item.getValue());});} +record.set(f.name,value);}},this);record.endEdit();return this;},loadRecord:function(record){this.setValues(record.data);return this;},beforeAction:function(action){this.items.each(function(f){if(f.isFormField&&f.syncValue){f.syncValue();}});var o=action.options;if(o.waitMsg){if(this.waitMsgTarget===true){this.el.mask(o.waitMsg,'x-mask-loading');}else if(this.waitMsgTarget){this.waitMsgTarget=Ext.get(this.waitMsgTarget);this.waitMsgTarget.mask(o.waitMsg,'x-mask-loading');}else{Ext.MessageBox.wait(o.waitMsg,o.waitTitle||this.waitTitle);}}},afterAction:function(action,success){this.activeAction=null;var o=action.options;if(o.waitMsg){if(this.waitMsgTarget===true){this.el.unmask();}else if(this.waitMsgTarget){this.waitMsgTarget.unmask();}else{Ext.MessageBox.updateProgress(1);Ext.MessageBox.hide();}} +if(success){if(o.reset){this.reset();} +Ext.callback(o.success,o.scope,[this,action]);this.fireEvent('actioncomplete',this,action);}else{Ext.callback(o.failure,o.scope,[this,action]);this.fireEvent('actionfailed',this,action);}},findField:function(id){var field=this.items.get(id);if(!Ext.isObject(field)){var findMatchingField=function(f){if(f.isFormField){if(f.dataIndex==id||f.id==id||f.getName()==id){field=f;return false;}else if(f.isComposite){return f.items.each(findMatchingField);}else if(f instanceof Ext.form.CheckboxGroup&&f.rendered){return f.eachItem(findMatchingField);}}};this.items.each(findMatchingField);} +return field||null;},markInvalid:function(errors){if(Ext.isArray(errors)){for(var i=0,len=errors.length;i<len;i++){var fieldError=errors[i];var f=this.findField(fieldError.id);if(f){f.markInvalid(fieldError.msg);}}}else{var field,id;for(id in errors){if(!Ext.isFunction(errors[id])&&(field=this.findField(id))){field.markInvalid(errors[id]);}}} +return this;},setValues:function(values){if(Ext.isArray(values)){for(var i=0,len=values.length;i<len;i++){var v=values[i];var f=this.findField(v.id);if(f){f.setValue(v.value);if(this.trackResetOnLoad){f.originalValue=f.getValue();}}}}else{var field,id;for(id in values){if(!Ext.isFunction(values[id])&&(field=this.findField(id))){field.setValue(values[id]);if(this.trackResetOnLoad){field.originalValue=field.getValue();}}}} +return this;},getValues:function(asString){var fs=Ext.lib.Ajax.serializeForm(this.el.dom);if(asString===true){return fs;} +return Ext.urlDecode(fs);},getFieldValues:function(dirtyOnly){var o={},n,key,val;this.items.each(function(f){if(!f.disabled&&(dirtyOnly!==true||f.isDirty())){n=f.getName();key=o[n];val=f.getValue();if(Ext.isDefined(key)){if(Ext.isArray(key)){o[n].push(val);}else{o[n]=[key,val];}}else{o[n]=val;}}});return o;},clearInvalid:function(){this.items.each(function(f){f.clearInvalid();});return this;},reset:function(){this.items.each(function(f){f.reset();});return this;},add:function(){this.items.addAll(Array.prototype.slice.call(arguments,0));return this;},remove:function(field){this.items.remove(field);return this;},cleanDestroyed:function(){this.items.filterBy(function(o){return!!o.isDestroyed;}).each(this.remove,this);},render:function(){this.items.each(function(f){if(f.isFormField&&!f.rendered&&document.getElementById(f.id)){f.applyToMarkup(f.id);}});return this;},applyToFields:function(o){this.items.each(function(f){Ext.apply(f,o);});return this;},applyIfToFields:function(o){this.items.each(function(f){Ext.applyIf(f,o);});return this;},callFieldMethod:function(fnName,args){args=args||[];this.items.each(function(f){if(Ext.isFunction(f[fnName])){f[fnName].apply(f,args);}});return this;}});Ext.BasicForm=Ext.form.BasicForm;Ext.FormPanel=Ext.extend(Ext.Panel,{minButtonWidth:75,labelAlign:'left',monitorValid:false,monitorPoll:200,layout:'form',initComponent:function(){this.form=this.createForm();Ext.FormPanel.superclass.initComponent.call(this);this.bodyCfg={tag:'form',cls:this.baseCls+'-body',method:this.method||'POST',id:this.formId||Ext.id()};if(this.fileUpload){this.bodyCfg.enctype='multipart/form-data';} +this.initItems();this.addEvents('clientvalidation');this.relayEvents(this.form,['beforeaction','actionfailed','actioncomplete']);},createForm:function(){var config=Ext.applyIf({listeners:{}},this.initialConfig);return new Ext.form.BasicForm(null,config);},initFields:function(){var f=this.form;var formPanel=this;var fn=function(c){if(formPanel.isField(c)){f.add(c);}else if(c.findBy&&c!=formPanel){formPanel.applySettings(c);if(c.items&&c.items.each){c.items.each(fn,this);}}};this.items.each(fn,this);},applySettings:function(c){var ct=c.ownerCt;Ext.applyIf(c,{labelAlign:ct.labelAlign,labelWidth:ct.labelWidth,itemCls:ct.itemCls});},getLayoutTarget:function(){return this.form.el;},getForm:function(){return this.form;},onRender:function(ct,position){this.initFields();Ext.FormPanel.superclass.onRender.call(this,ct,position);this.form.initEl(this.body);},beforeDestroy:function(){this.stopMonitoring();this.form.destroy(true);Ext.FormPanel.superclass.beforeDestroy.call(this);},isField:function(c){return!!c.setValue&&!!c.getValue&&!!c.markInvalid&&!!c.clearInvalid;},initEvents:function(){Ext.FormPanel.superclass.initEvents.call(this);this.on({scope:this,add:this.onAddEvent,remove:this.onRemoveEvent});if(this.monitorValid){this.startMonitoring();}},onAdd:function(c){Ext.FormPanel.superclass.onAdd.call(this,c);this.processAdd(c);},onAddEvent:function(ct,c){if(ct!==this){this.processAdd(c);}},processAdd:function(c){if(this.isField(c)){this.form.add(c);}else if(c.findBy){this.applySettings(c);this.form.add.apply(this.form,c.findBy(this.isField));}},onRemove:function(c){Ext.FormPanel.superclass.onRemove.call(this,c);this.processRemove(c);},onRemoveEvent:function(ct,c){if(ct!==this){this.processRemove(c);}},processRemove:function(c){if(!this.destroying){if(this.isField(c)){this.form.remove(c);}else if(c.findBy){Ext.each(c.findBy(this.isField),this.form.remove,this.form);this.form.cleanDestroyed();}}},startMonitoring:function(){if(!this.validTask){this.validTask=new Ext.util.TaskRunner();this.validTask.start({run:this.bindHandler,interval:this.monitorPoll||200,scope:this});}},stopMonitoring:function(){if(this.validTask){this.validTask.stopAll();this.validTask=null;}},load:function(){this.form.load.apply(this.form,arguments);},onDisable:function(){Ext.FormPanel.superclass.onDisable.call(this);if(this.form){this.form.items.each(function(){this.disable();});}},onEnable:function(){Ext.FormPanel.superclass.onEnable.call(this);if(this.form){this.form.items.each(function(){this.enable();});}},bindHandler:function(){var valid=true;this.form.items.each(function(f){if(!f.isValid(true)){valid=false;return false;}});if(this.fbar){var fitems=this.fbar.items.items;for(var i=0,len=fitems.length;i<len;i++){var btn=fitems[i];if(btn.formBind===true&&btn.disabled===valid){btn.setDisabled(!valid);}}} +this.fireEvent('clientvalidation',this,valid);}});Ext.reg('form',Ext.FormPanel);Ext.form.FormPanel=Ext.FormPanel;Ext.form.FieldSet=Ext.extend(Ext.Panel,{baseCls:'x-fieldset',layout:'form',animCollapse:false,onRender:function(ct,position){if(!this.el){this.el=document.createElement('fieldset');this.el.id=this.id;if(this.title||this.header||this.checkboxToggle){this.el.appendChild(document.createElement('legend')).className=this.baseCls+'-header';}} +Ext.form.FieldSet.superclass.onRender.call(this,ct,position);if(this.checkboxToggle){var o=typeof this.checkboxToggle=='object'?this.checkboxToggle:{tag:'input',type:'checkbox',name:this.checkboxName||this.id+'-checkbox'};this.checkbox=this.header.insertFirst(o);this.checkbox.dom.checked=!this.collapsed;this.mon(this.checkbox,'click',this.onCheckClick,this);}},onCollapse:function(doAnim,animArg){if(this.checkbox){this.checkbox.dom.checked=false;} +Ext.form.FieldSet.superclass.onCollapse.call(this,doAnim,animArg);},onExpand:function(doAnim,animArg){if(this.checkbox){this.checkbox.dom.checked=true;} +Ext.form.FieldSet.superclass.onExpand.call(this,doAnim,animArg);},onCheckClick:function(){this[this.checkbox.dom.checked?'expand':'collapse']();}});Ext.reg('fieldset',Ext.form.FieldSet);Ext.form.HtmlEditor=Ext.extend(Ext.form.Field,{enableFormat:true,enableFontSize:true,enableColors:true,enableAlignments:true,enableLists:true,enableSourceEdit:true,enableLinks:true,enableFont:true,createLinkText:'Please enter the URL for the link:',defaultLinkValue:'http:/'+'/',fontFamilies:['Arial','Courier New','Tahoma','Times New Roman','Verdana'],defaultFont:'tahoma',defaultValue:(Ext.isOpera||Ext.isIE6)?' ':'​',actionMode:'wrap',validationEvent:false,deferHeight:true,initialized:false,activated:false,sourceEditMode:false,onFocus:Ext.emptyFn,iframePad:3,hideMode:'offsets',defaultAutoCreate:{tag:"textarea",style:"width:500px;height:300px;",autocomplete:"off"},initComponent:function(){this.addEvents('initialize','activate','beforesync','beforepush','sync','push','editmodechange');Ext.form.HtmlEditor.superclass.initComponent.call(this);},createFontOptions:function(){var buf=[],fs=this.fontFamilies,ff,lc;for(var i=0,len=fs.length;i<len;i++){ff=fs[i];lc=ff.toLowerCase();buf.push('<option value="',lc,'" style="font-family:',ff,';"',(this.defaultFont==lc?' selected="true">':'>'),ff,'</option>');} +return buf.join('');},createToolbar:function(editor){var items=[];var tipsEnabled=Ext.QuickTips&&Ext.QuickTips.isEnabled();function btn(id,toggle,handler){return{itemId:id,cls:'x-btn-icon',iconCls:'x-edit-'+id,enableToggle:toggle!==false,scope:editor,handler:handler||editor.relayBtnCmd,clickEvent:'mousedown',tooltip:tipsEnabled?editor.buttonTips[id]||undefined:undefined,overflowText:editor.buttonTips[id].title||undefined,tabIndex:-1};} +if(this.enableFont&&!Ext.isSafari2){var fontSelectItem=new Ext.Toolbar.Item({autoEl:{tag:'select',cls:'x-font-select',html:this.createFontOptions()}});items.push(fontSelectItem,'-');} +if(this.enableFormat){items.push(btn('bold'),btn('italic'),btn('underline'));} +if(this.enableFontSize){items.push('-',btn('increasefontsize',false,this.adjustFont),btn('decreasefontsize',false,this.adjustFont));} +if(this.enableColors){items.push('-',{itemId:'forecolor',cls:'x-btn-icon',iconCls:'x-edit-forecolor',clickEvent:'mousedown',tooltip:tipsEnabled?editor.buttonTips.forecolor||undefined:undefined,tabIndex:-1,menu:new Ext.menu.ColorMenu({allowReselect:true,focus:Ext.emptyFn,value:'000000',plain:true,listeners:{scope:this,select:function(cp,color){this.execCmd('forecolor',Ext.isWebKit||Ext.isIE?'#'+color:color);this.deferFocus();}},clickEvent:'mousedown'})},{itemId:'backcolor',cls:'x-btn-icon',iconCls:'x-edit-backcolor',clickEvent:'mousedown',tooltip:tipsEnabled?editor.buttonTips.backcolor||undefined:undefined,tabIndex:-1,menu:new Ext.menu.ColorMenu({focus:Ext.emptyFn,value:'FFFFFF',plain:true,allowReselect:true,listeners:{scope:this,select:function(cp,color){if(Ext.isGecko){this.execCmd('useCSS',false);this.execCmd('hilitecolor',color);this.execCmd('useCSS',true);this.deferFocus();}else{this.execCmd(Ext.isOpera?'hilitecolor':'backcolor',Ext.isWebKit||Ext.isIE?'#'+color:color);this.deferFocus();}}},clickEvent:'mousedown'})});} +if(this.enableAlignments){items.push('-',btn('justifyleft'),btn('justifycenter'),btn('justifyright'));} +if(!Ext.isSafari2){if(this.enableLinks){items.push('-',btn('createlink',false,this.createLink));} +if(this.enableLists){items.push('-',btn('insertorderedlist'),btn('insertunorderedlist'));} +if(this.enableSourceEdit){items.push('-',btn('sourceedit',true,function(btn){this.toggleSourceEdit(!this.sourceEditMode);}));}} +var tb=new Ext.Toolbar({renderTo:this.wrap.dom.firstChild,items:items});if(fontSelectItem){this.fontSelect=fontSelectItem.el;this.mon(this.fontSelect,'change',function(){var font=this.fontSelect.dom.value;this.relayCmd('fontname',font);this.deferFocus();},this);} +this.mon(tb.el,'click',function(e){e.preventDefault();});this.tb=tb;this.tb.doLayout();},onDisable:function(){this.wrap.mask();Ext.form.HtmlEditor.superclass.onDisable.call(this);},onEnable:function(){this.wrap.unmask();Ext.form.HtmlEditor.superclass.onEnable.call(this);},setReadOnly:function(readOnly){Ext.form.HtmlEditor.superclass.setReadOnly.call(this,readOnly);if(this.initialized){if(Ext.isIE){this.getEditorBody().contentEditable=!readOnly;}else{this.setDesignMode(!readOnly);} +var bd=this.getEditorBody();if(bd){bd.style.cursor=this.readOnly?'default':'text';} +this.disableItems(readOnly);}},getDocMarkup:function(){var h=Ext.fly(this.iframe).getHeight()-this.iframePad*2;return String.format('<html><head><style type="text/css">body{border: 0; margin: 0; padding: {0}px; height: {1}px; cursor: text}</style></head><body></body></html>',this.iframePad,h);},getEditorBody:function(){var doc=this.getDoc();return doc.body||doc.documentElement;},getDoc:function(){return Ext.isIE?this.getWin().document:(this.iframe.contentDocument||this.getWin().document);},getWin:function(){return Ext.isIE?this.iframe.contentWindow:window.frames[this.iframe.name];},onRender:function(ct,position){Ext.form.HtmlEditor.superclass.onRender.call(this,ct,position);this.el.dom.style.border='0 none';this.el.dom.setAttribute('tabIndex',-1);this.el.addClass('x-hidden');if(Ext.isIE){this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;');} +this.wrap=this.el.wrap({cls:'x-html-editor-wrap',cn:{cls:'x-html-editor-tb'}});this.createToolbar(this);this.disableItems(true);this.tb.doLayout();this.createIFrame();if(!this.width){var sz=this.el.getSize();this.setSize(sz.width,this.height||sz.height);} +this.resizeEl=this.positionEl=this.wrap;},createIFrame:function(){var iframe=document.createElement('iframe');iframe.name=Ext.id();iframe.frameBorder='0';iframe.style.overflow='auto';iframe.src=Ext.SSL_SECURE_URL;this.wrap.dom.appendChild(iframe);this.iframe=iframe;this.monitorTask=Ext.TaskMgr.start({run:this.checkDesignMode,scope:this,interval:100});},initFrame:function(){Ext.TaskMgr.stop(this.monitorTask);var doc=this.getDoc();this.win=this.getWin();doc.open();doc.write(this.getDocMarkup());doc.close();var task={run:function(){var doc=this.getDoc();if(doc.body||doc.readyState=='complete'){Ext.TaskMgr.stop(task);this.setDesignMode(true);this.initEditor.defer(10,this);}},interval:10,duration:10000,scope:this};Ext.TaskMgr.start(task);},checkDesignMode:function(){if(this.wrap&&this.wrap.dom.offsetWidth){var doc=this.getDoc();if(!doc){return;} +if(!doc.editorInitialized||this.getDesignMode()!='on'){this.initFrame();}}},setDesignMode:function(mode){var doc=this.getDoc();if(doc){if(this.readOnly){mode=false;} +doc.designMode=(/on|true/i).test(String(mode).toLowerCase())?'on':'off';}},getDesignMode:function(){var doc=this.getDoc();if(!doc){return'';} +return String(doc.designMode).toLowerCase();},disableItems:function(disabled){if(this.fontSelect){this.fontSelect.dom.disabled=disabled;} +this.tb.items.each(function(item){if(item.getItemId()!='sourceedit'){item.setDisabled(disabled);}});},onResize:function(w,h){Ext.form.HtmlEditor.superclass.onResize.apply(this,arguments);if(this.el&&this.iframe){if(Ext.isNumber(w)){var aw=w-this.wrap.getFrameWidth('lr');this.el.setWidth(aw);this.tb.setWidth(aw);this.iframe.style.width=Math.max(aw,0)+'px';} +if(Ext.isNumber(h)){var ah=h-this.wrap.getFrameWidth('tb')-this.tb.el.getHeight();this.el.setHeight(ah);this.iframe.style.height=Math.max(ah,0)+'px';var bd=this.getEditorBody();if(bd){bd.style.height=Math.max((ah-(this.iframePad*2)),0)+'px';}}}},toggleSourceEdit:function(sourceEditMode){var iframeHeight,elHeight;if(sourceEditMode===undefined){sourceEditMode=!this.sourceEditMode;} +this.sourceEditMode=sourceEditMode===true;var btn=this.tb.getComponent('sourceedit');if(btn.pressed!==this.sourceEditMode){btn.toggle(this.sourceEditMode);if(!btn.xtbHidden){return;}} +if(this.sourceEditMode){this.previousSize=this.getSize();iframeHeight=Ext.get(this.iframe).getHeight();this.disableItems(true);this.syncValue();this.iframe.className='x-hidden';this.el.removeClass('x-hidden');this.el.dom.removeAttribute('tabIndex');this.el.focus();this.el.dom.style.height=iframeHeight+'px';} +else{elHeight=parseInt(this.el.dom.style.height,10);if(this.initialized){this.disableItems(this.readOnly);} +this.pushValue();this.iframe.className='';this.el.addClass('x-hidden');this.el.dom.setAttribute('tabIndex',-1);this.deferFocus();this.setSize(this.previousSize);delete this.previousSize;this.iframe.style.height=elHeight+'px';} +this.fireEvent('editmodechange',this,this.sourceEditMode);},createLink:function(){var url=prompt(this.createLinkText,this.defaultLinkValue);if(url&&url!='http:/'+'/'){this.relayCmd('createlink',url);}},initEvents:function(){this.originalValue=this.getValue();},markInvalid:Ext.emptyFn,clearInvalid:Ext.emptyFn,setValue:function(v){Ext.form.HtmlEditor.superclass.setValue.call(this,v);this.pushValue();return this;},cleanHtml:function(html){html=String(html);if(Ext.isWebKit){html=html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi,'');} +if(html.charCodeAt(0)==this.defaultValue.replace(/\D/g,'')){html=html.substring(1);} +return html;},syncValue:function(){if(this.initialized){var bd=this.getEditorBody();var html=bd.innerHTML;if(Ext.isWebKit){var bs=bd.getAttribute('style');var m=bs.match(/text-align:(.*?);/i);if(m&&m[1]){html='<div style="'+m[0]+'">'+html+'</div>';}} +html=this.cleanHtml(html);if(this.fireEvent('beforesync',this,html)!==false){this.el.dom.value=html;this.fireEvent('sync',this,html);}}},getValue:function(){this[this.sourceEditMode?'pushValue':'syncValue']();return Ext.form.HtmlEditor.superclass.getValue.call(this);},pushValue:function(){if(this.initialized){var v=this.el.dom.value;if(!this.activated&&v.length<1){v=this.defaultValue;} +if(this.fireEvent('beforepush',this,v)!==false){this.getEditorBody().innerHTML=v;if(Ext.isGecko){this.setDesignMode(false);this.setDesignMode(true);} +this.fireEvent('push',this,v);}}},deferFocus:function(){this.focus.defer(10,this);},focus:function(){if(this.win&&!this.sourceEditMode){this.win.focus();}else{this.el.focus();}},initEditor:function(){try{var dbody=this.getEditorBody(),ss=this.el.getStyles('font-size','font-family','background-image','background-repeat','background-color','color'),doc,fn;ss['background-attachment']='fixed';dbody.bgProperties='fixed';Ext.DomHelper.applyStyles(dbody,ss);doc=this.getDoc();if(doc){try{Ext.EventManager.removeAll(doc);}catch(e){}} +fn=this.onEditorEvent.createDelegate(this);Ext.EventManager.on(doc,{mousedown:fn,dblclick:fn,click:fn,keyup:fn,buffer:100});if(Ext.isGecko){Ext.EventManager.on(doc,'keypress',this.applyCommand,this);} +if(Ext.isIE||Ext.isWebKit||Ext.isOpera){Ext.EventManager.on(doc,'keydown',this.fixKeys,this);} +doc.editorInitialized=true;this.initialized=true;this.pushValue();this.setReadOnly(this.readOnly);this.fireEvent('initialize',this);}catch(e){}},beforeDestroy:function(){if(this.monitorTask){Ext.TaskMgr.stop(this.monitorTask);} +if(this.rendered){Ext.destroy(this.tb);var doc=this.getDoc();if(doc){try{Ext.EventManager.removeAll(doc);for(var prop in doc){delete doc[prop];}}catch(e){}} +if(this.wrap){this.wrap.dom.innerHTML='';this.wrap.remove();}} +Ext.form.HtmlEditor.superclass.beforeDestroy.call(this);},onFirstFocus:function(){this.activated=true;this.disableItems(this.readOnly);if(Ext.isGecko){this.win.focus();var s=this.win.getSelection();if(!s.focusNode||s.focusNode.nodeType!=3){var r=s.getRangeAt(0);r.selectNodeContents(this.getEditorBody());r.collapse(true);this.deferFocus();} +try{this.execCmd('useCSS',true);this.execCmd('styleWithCSS',false);}catch(e){}} +this.fireEvent('activate',this);},adjustFont:function(btn){var adjust=btn.getItemId()=='increasefontsize'?1:-1,doc=this.getDoc(),v=parseInt(doc.queryCommandValue('FontSize')||2,10);if((Ext.isSafari&&!Ext.isSafari2)||Ext.isChrome||Ext.isAir){if(v<=10){v=1+adjust;}else if(v<=13){v=2+adjust;}else if(v<=16){v=3+adjust;}else if(v<=18){v=4+adjust;}else if(v<=24){v=5+adjust;}else{v=6+adjust;} +v=v.constrain(1,6);}else{if(Ext.isSafari){adjust*=2;} +v=Math.max(1,v+adjust)+(Ext.isSafari?'px':0);} +this.execCmd('FontSize',v);},onEditorEvent:function(e){this.updateToolbar();},updateToolbar:function(){if(this.readOnly){return;} +if(!this.activated){this.onFirstFocus();return;} +var btns=this.tb.items.map,doc=this.getDoc();if(this.enableFont&&!Ext.isSafari2){var name=(doc.queryCommandValue('FontName')||this.defaultFont).toLowerCase();if(name!=this.fontSelect.dom.value){this.fontSelect.dom.value=name;}} +if(this.enableFormat){btns.bold.toggle(doc.queryCommandState('bold'));btns.italic.toggle(doc.queryCommandState('italic'));btns.underline.toggle(doc.queryCommandState('underline'));} +if(this.enableAlignments){btns.justifyleft.toggle(doc.queryCommandState('justifyleft'));btns.justifycenter.toggle(doc.queryCommandState('justifycenter'));btns.justifyright.toggle(doc.queryCommandState('justifyright'));} +if(!Ext.isSafari2&&this.enableLists){btns.insertorderedlist.toggle(doc.queryCommandState('insertorderedlist'));btns.insertunorderedlist.toggle(doc.queryCommandState('insertunorderedlist'));} +Ext.menu.MenuMgr.hideAll();this.syncValue();},relayBtnCmd:function(btn){this.relayCmd(btn.getItemId());},relayCmd:function(cmd,value){(function(){this.focus();this.execCmd(cmd,value);this.updateToolbar();}).defer(10,this);},execCmd:function(cmd,value){var doc=this.getDoc();doc.execCommand(cmd,false,value===undefined?null:value);this.syncValue();},applyCommand:function(e){if(e.ctrlKey){var c=e.getCharCode(),cmd;if(c>0){c=String.fromCharCode(c);switch(c){case'b':cmd='bold';break;case'i':cmd='italic';break;case'u':cmd='underline';break;} +if(cmd){this.win.focus();this.execCmd(cmd);this.deferFocus();e.preventDefault();}}}},insertAtCursor:function(text){if(!this.activated){return;} +if(Ext.isIE){this.win.focus();var doc=this.getDoc(),r=doc.selection.createRange();if(r){r.pasteHTML(text);this.syncValue();this.deferFocus();}}else{this.win.focus();this.execCmd('InsertHTML',text);this.deferFocus();}},fixKeys:function(){if(Ext.isIE){return function(e){var k=e.getKey(),doc=this.getDoc(),r;if(k==e.TAB){e.stopEvent();r=doc.selection.createRange();if(r){r.collapse(true);r.pasteHTML(' ');this.deferFocus();}}else if(k==e.ENTER){r=doc.selection.createRange();if(r){var target=r.parentElement();if(!target||target.tagName.toLowerCase()!='li'){e.stopEvent();r.pasteHTML('<br />');r.collapse(false);r.select();}}}};}else if(Ext.isOpera){return function(e){var k=e.getKey();if(k==e.TAB){e.stopEvent();this.win.focus();this.execCmd('InsertHTML',' ');this.deferFocus();}};}else if(Ext.isWebKit){return function(e){var k=e.getKey();if(k==e.TAB){e.stopEvent();this.execCmd('InsertText','\t');this.deferFocus();}else if(k==e.ENTER){e.stopEvent();this.execCmd('InsertHtml','<br /><br />');this.deferFocus();}};}}(),getToolbar:function(){return this.tb;},buttonTips:{bold:{title:'Bold (Ctrl+B)',text:'Make the selected text bold.',cls:'x-html-editor-tip'},italic:{title:'Italic (Ctrl+I)',text:'Make the selected text italic.',cls:'x-html-editor-tip'},underline:{title:'Underline (Ctrl+U)',text:'Underline the selected text.',cls:'x-html-editor-tip'},increasefontsize:{title:'Grow Text',text:'Increase the font size.',cls:'x-html-editor-tip'},decreasefontsize:{title:'Shrink Text',text:'Decrease the font size.',cls:'x-html-editor-tip'},backcolor:{title:'Text Highlight Color',text:'Change the background color of the selected text.',cls:'x-html-editor-tip'},forecolor:{title:'Font Color',text:'Change the color of the selected text.',cls:'x-html-editor-tip'},justifyleft:{title:'Align Text Left',text:'Align text to the left.',cls:'x-html-editor-tip'},justifycenter:{title:'Center Text',text:'Center text in the editor.',cls:'x-html-editor-tip'},justifyright:{title:'Align Text Right',text:'Align text to the right.',cls:'x-html-editor-tip'},insertunorderedlist:{title:'Bullet List',text:'Start a bulleted list.',cls:'x-html-editor-tip'},insertorderedlist:{title:'Numbered List',text:'Start a numbered list.',cls:'x-html-editor-tip'},createlink:{title:'Hyperlink',text:'Make the selected text a hyperlink.',cls:'x-html-editor-tip'},sourceedit:{title:'Source Edit',text:'Switch to source editing mode.',cls:'x-html-editor-tip'}}});Ext.reg('htmleditor',Ext.form.HtmlEditor);Ext.form.TimeField=Ext.extend(Ext.form.ComboBox,{minValue:undefined,maxValue:undefined,minText:"The time in this field must be equal to or after {0}",maxText:"The time in this field must be equal to or before {0}",invalidText:"{0} is not a valid time",format:"g:i A",altFormats:"g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",increment:15,mode:'local',triggerAction:'all',typeAhead:false,initDate:'1/1/2008',initDateFormat:'j/n/Y',initComponent:function(){if(Ext.isDefined(this.minValue)){this.setMinValue(this.minValue,true);} +if(Ext.isDefined(this.maxValue)){this.setMaxValue(this.maxValue,true);} +if(!this.store){this.generateStore(true);} +Ext.form.TimeField.superclass.initComponent.call(this);},setMinValue:function(value,initial){this.setLimit(value,true,initial);return this;},setMaxValue:function(value,initial){this.setLimit(value,false,initial);return this;},generateStore:function(initial){var min=this.minValue||new Date(this.initDate).clearTime(),max=this.maxValue||new Date(this.initDate).clearTime().add('mi',(24*60)-1),times=[];while(min<=max){times.push(min.dateFormat(this.format));min=min.add('mi',this.increment);} +this.bindStore(times,initial);},setLimit:function(value,isMin,initial){var d;if(Ext.isString(value)){d=this.parseDate(value);}else if(Ext.isDate(value)){d=value;} +if(d){var val=new Date(this.initDate).clearTime();val.setHours(d.getHours(),d.getMinutes(),d.getSeconds(),d.getMilliseconds());this[isMin?'minValue':'maxValue']=val;if(!initial){this.generateStore();}}},getValue:function(){var v=Ext.form.TimeField.superclass.getValue.call(this);return this.formatDate(this.parseDate(v))||'';},setValue:function(value){return Ext.form.TimeField.superclass.setValue.call(this,this.formatDate(this.parseDate(value)));},validateValue:Ext.form.DateField.prototype.validateValue,formatDate:Ext.form.DateField.prototype.formatDate,parseDate:function(value){if(!value||Ext.isDate(value)){return value;} +var id=this.initDate+' ',idf=this.initDateFormat+' ',v=Date.parseDate(id+value,idf+this.format),af=this.altFormats;if(!v&&af){if(!this.altFormatsArray){this.altFormatsArray=af.split("|");} +for(var i=0,afa=this.altFormatsArray,len=afa.length;i<len&&!v;i++){v=Date.parseDate(id+value,idf+afa[i]);}} +return v;}});Ext.reg('timefield',Ext.form.TimeField);Ext.form.SliderField=Ext.extend(Ext.form.Field,{useTips:true,tipText:null,actionMode:'wrap',initComponent:function(){var cfg=Ext.copyTo({id:this.id+'-slider'},this.initialConfig,['vertical','minValue','maxValue','decimalPrecision','keyIncrement','increment','clickToChange','animate']);if(this.useTips){var plug=this.tipText?{getText:this.tipText}:{};cfg.plugins=[new Ext.slider.Tip(plug)];} +this.slider=new Ext.Slider(cfg);Ext.form.SliderField.superclass.initComponent.call(this);},onRender:function(ct,position){this.autoCreate={id:this.id,name:this.name,type:'hidden',tag:'input'};Ext.form.SliderField.superclass.onRender.call(this,ct,position);this.wrap=this.el.wrap({cls:'x-form-field-wrap'});this.resizeEl=this.positionEl=this.wrap;this.slider.render(this.wrap);},onResize:function(w,h,aw,ah){Ext.form.SliderField.superclass.onResize.call(this,w,h,aw,ah);this.slider.setSize(w,h);},initEvents:function(){Ext.form.SliderField.superclass.initEvents.call(this);this.slider.on('change',this.onChange,this);},onChange:function(slider,v){this.setValue(v,undefined,true);},onEnable:function(){Ext.form.SliderField.superclass.onEnable.call(this);this.slider.enable();},onDisable:function(){Ext.form.SliderField.superclass.onDisable.call(this);this.slider.disable();},beforeDestroy:function(){Ext.destroy(this.slider);Ext.form.SliderField.superclass.beforeDestroy.call(this);},alignErrorIcon:function(){this.errorIcon.alignTo(this.slider.el,'tl-tr',[2,0]);},setMinValue:function(v){this.slider.setMinValue(v);return this;},setMaxValue:function(v){this.slider.setMaxValue(v);return this;},setValue:function(v,animate,silent){if(!silent){this.slider.setValue(v,animate);} +return Ext.form.SliderField.superclass.setValue.call(this,this.slider.getValue());},getValue:function(){return this.slider.getValue();}});Ext.reg('sliderfield',Ext.form.SliderField);Ext.form.Label=Ext.extend(Ext.BoxComponent,{onRender:function(ct,position){if(!this.el){this.el=document.createElement('label');this.el.id=this.getId();this.el.innerHTML=this.text?Ext.util.Format.htmlEncode(this.text):(this.html||'');if(this.forId){this.el.setAttribute('for',this.forId);}} +Ext.form.Label.superclass.onRender.call(this,ct,position);},setText:function(t,encode){var e=encode===false;this[!e?'text':'html']=t;delete this[e?'text':'html'];if(this.rendered){this.el.dom.innerHTML=encode!==false?Ext.util.Format.htmlEncode(t):t;} +return this;}});Ext.reg('label',Ext.form.Label);Ext.form.Action=function(form,options){this.form=form;this.options=options||{};};Ext.form.Action.CLIENT_INVALID='client';Ext.form.Action.SERVER_INVALID='server';Ext.form.Action.CONNECT_FAILURE='connect';Ext.form.Action.LOAD_FAILURE='load';Ext.form.Action.prototype={type:'default',run:function(options){},success:function(response){},handleResponse:function(response){},failure:function(response){this.response=response;this.failureType=Ext.form.Action.CONNECT_FAILURE;this.form.afterAction(this,false);},processResponse:function(response){this.response=response;if(!response.responseText&&!response.responseXML){return true;} +this.result=this.handleResponse(response);return this.result;},getUrl:function(appendParams){var url=this.options.url||this.form.url||this.form.el.dom.action;if(appendParams){var p=this.getParams();if(p){url=Ext.urlAppend(url,p);}} +return url;},getMethod:function(){return(this.options.method||this.form.method||this.form.el.dom.method||'POST').toUpperCase();},getParams:function(){var bp=this.form.baseParams;var p=this.options.params;if(p){if(typeof p=="object"){p=Ext.urlEncode(Ext.applyIf(p,bp));}else if(typeof p=='string'&&bp){p+='&'+Ext.urlEncode(bp);}}else if(bp){p=Ext.urlEncode(bp);} +return p;},createCallback:function(opts){var opts=opts||{};return{success:this.success,failure:this.failure,scope:this,timeout:(opts.timeout*1000)||(this.form.timeout*1000),upload:this.form.fileUpload?this.success:undefined};}};Ext.form.Action.Submit=function(form,options){Ext.form.Action.Submit.superclass.constructor.call(this,form,options);};Ext.extend(Ext.form.Action.Submit,Ext.form.Action,{type:'submit',run:function(){var o=this.options,method=this.getMethod(),isGet=method=='GET';if(o.clientValidation===false||this.form.isValid()){if(o.submitEmptyText===false){var fields=this.form.items,emptyFields=[],setupEmptyFields=function(f){if(f.el.getValue()==f.emptyText){emptyFields.push(f);f.el.dom.value="";} +if(f.isComposite&&f.rendered){f.items.each(setupEmptyFields);}};fields.each(setupEmptyFields);} +Ext.Ajax.request(Ext.apply(this.createCallback(o),{form:this.form.el.dom,url:this.getUrl(isGet),method:method,headers:o.headers,params:!isGet?this.getParams():null,isUpload:this.form.fileUpload}));if(o.submitEmptyText===false){Ext.each(emptyFields,function(f){if(f.applyEmptyText){f.applyEmptyText();}});}}else if(o.clientValidation!==false){this.failureType=Ext.form.Action.CLIENT_INVALID;this.form.afterAction(this,false);}},success:function(response){var result=this.processResponse(response);if(result===true||result.success){this.form.afterAction(this,true);return;} +if(result.errors){this.form.markInvalid(result.errors);} +this.failureType=Ext.form.Action.SERVER_INVALID;this.form.afterAction(this,false);},handleResponse:function(response){if(this.form.errorReader){var rs=this.form.errorReader.read(response);var errors=[];if(rs.records){for(var i=0,len=rs.records.length;i<len;i++){var r=rs.records[i];errors[i]=r.data;}} +if(errors.length<1){errors=null;} +return{success:rs.success,errors:errors};} +return Ext.decode(response.responseText);}});Ext.form.Action.Load=function(form,options){Ext.form.Action.Load.superclass.constructor.call(this,form,options);this.reader=this.form.reader;};Ext.extend(Ext.form.Action.Load,Ext.form.Action,{type:'load',run:function(){Ext.Ajax.request(Ext.apply(this.createCallback(this.options),{method:this.getMethod(),url:this.getUrl(false),headers:this.options.headers,params:this.getParams()}));},success:function(response){var result=this.processResponse(response);if(result===true||!result.success||!result.data){this.failureType=Ext.form.Action.LOAD_FAILURE;this.form.afterAction(this,false);return;} +this.form.clearInvalid();this.form.setValues(result.data);this.form.afterAction(this,true);},handleResponse:function(response){if(this.form.reader){var rs=this.form.reader.read(response);var data=rs.records&&rs.records[0]?rs.records[0].data:null;return{success:rs.success,data:data};} +return Ext.decode(response.responseText);}});Ext.form.Action.DirectLoad=Ext.extend(Ext.form.Action.Load,{constructor:function(form,opts){Ext.form.Action.DirectLoad.superclass.constructor.call(this,form,opts);},type:'directload',run:function(){var args=this.getParams();args.push(this.success,this);this.form.api.load.apply(window,args);},getParams:function(){var buf=[],o={};var bp=this.form.baseParams;var p=this.options.params;Ext.apply(o,p,bp);var paramOrder=this.form.paramOrder;if(paramOrder){for(var i=0,len=paramOrder.length;i<len;i++){buf.push(o[paramOrder[i]]);}}else if(this.form.paramsAsHash){buf.push(o);} +return buf;},processResponse:function(result){this.result=result;return result;},success:function(response,trans){if(trans.type==Ext.Direct.exceptions.SERVER){response={};} +Ext.form.Action.DirectLoad.superclass.success.call(this,response);}});Ext.form.Action.DirectSubmit=Ext.extend(Ext.form.Action.Submit,{constructor:function(form,opts){Ext.form.Action.DirectSubmit.superclass.constructor.call(this,form,opts);},type:'directsubmit',run:function(){var o=this.options;if(o.clientValidation===false||this.form.isValid()){this.success.params=this.getParams();this.form.api.submit(this.form.el.dom,this.success,this);}else if(o.clientValidation!==false){this.failureType=Ext.form.Action.CLIENT_INVALID;this.form.afterAction(this,false);}},getParams:function(){var o={};var bp=this.form.baseParams;var p=this.options.params;Ext.apply(o,p,bp);return o;},processResponse:function(result){this.result=result;return result;},success:function(response,trans){if(trans.type==Ext.Direct.exceptions.SERVER){response={};} +Ext.form.Action.DirectSubmit.superclass.success.call(this,response);}});Ext.form.Action.ACTION_TYPES={'load':Ext.form.Action.Load,'submit':Ext.form.Action.Submit,'directload':Ext.form.Action.DirectLoad,'directsubmit':Ext.form.Action.DirectSubmit};Ext.form.VTypes=function(){var alpha=/^[a-zA-Z_]+$/,alphanum=/^[a-zA-Z0-9_]+$/,email=/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/,url=/(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i;return{'email':function(v){return email.test(v);},'emailText':'This field should be an e-mail address in the format "user@example.com"','emailMask':/[a-z0-9_\.\-@\+]/i,'url':function(v){return url.test(v);},'urlText':'This field should be a URL in the format "http:/'+'/www.example.com"','alpha':function(v){return alpha.test(v);},'alphaText':'This field should only contain letters and _','alphaMask':/[a-z_]/i,'alphanum':function(v){return alphanum.test(v);},'alphanumText':'This field should only contain letters, numbers and _','alphanumMask':/[a-z0-9_]/i};}();Ext.Button=Ext.extend(Ext.BoxComponent,{hidden:false,disabled:false,pressed:false,enableToggle:false,menuAlign:'tl-bl?',type:'button',menuClassTarget:'tr:nth(2)',clickEvent:'click',handleMouseEvents:true,tooltipType:'qtip',buttonSelector:'button:first-child',scale:'small',iconAlign:'left',arrowAlign:'right',initComponent:function(){if(this.menu){this.menu=Ext.menu.MenuMgr.get(this.menu);this.menu.ownerCt=this;} +Ext.Button.superclass.initComponent.call(this);this.addEvents('click','toggle','mouseover','mouseout','menushow','menuhide','menutriggerover','menutriggerout');if(this.menu){this.menu.ownerCt=undefined;} +if(Ext.isString(this.toggleGroup)){this.enableToggle=true;}},getTemplateArgs:function(){return[this.type,'x-btn-'+this.scale+' x-btn-icon-'+this.scale+'-'+this.iconAlign,this.getMenuClass(),this.cls,this.id];},setButtonClass:function(){if(this.useSetClass){if(!Ext.isEmpty(this.oldCls)){this.el.removeClass([this.oldCls,'x-btn-pressed']);} +this.oldCls=(this.iconCls||this.icon)?(this.text?'x-btn-text-icon':'x-btn-icon'):'x-btn-noicon';this.el.addClass([this.oldCls,this.pressed?'x-btn-pressed':null]);}},getMenuClass:function(){return this.menu?(this.arrowAlign!='bottom'?'x-btn-arrow':'x-btn-arrow-bottom'):'';},onRender:function(ct,position){if(!this.template){if(!Ext.Button.buttonTemplate){Ext.Button.buttonTemplate=new Ext.Template('<table id="{4}" cellspacing="0" class="x-btn {3}"><tbody class="{1}">','<tr><td class="x-btn-tl"><i> </i></td><td class="x-btn-tc"></td><td class="x-btn-tr"><i> </i></td></tr>','<tr><td class="x-btn-ml"><i> </i></td><td class="x-btn-mc"><em class="{2}" unselectable="on"><button type="{0}"></button></em></td><td class="x-btn-mr"><i> </i></td></tr>','<tr><td class="x-btn-bl"><i> </i></td><td class="x-btn-bc"></td><td class="x-btn-br"><i> </i></td></tr>','</tbody></table>');Ext.Button.buttonTemplate.compile();} +this.template=Ext.Button.buttonTemplate;} +var btn,targs=this.getTemplateArgs();if(position){btn=this.template.insertBefore(position,targs,true);}else{btn=this.template.append(ct,targs,true);} +this.btnEl=btn.child(this.buttonSelector);this.mon(this.btnEl,{scope:this,focus:this.onFocus,blur:this.onBlur});this.initButtonEl(btn,this.btnEl);Ext.ButtonToggleMgr.register(this);},initButtonEl:function(btn,btnEl){this.el=btn;this.setIcon(this.icon);this.setText(this.text);this.setIconClass(this.iconCls);if(Ext.isDefined(this.tabIndex)){btnEl.dom.tabIndex=this.tabIndex;} +if(this.tooltip){this.setTooltip(this.tooltip,true);} +if(this.handleMouseEvents){this.mon(btn,{scope:this,mouseover:this.onMouseOver,mousedown:this.onMouseDown});} +if(this.menu){this.mon(this.menu,{scope:this,show:this.onMenuShow,hide:this.onMenuHide});} +if(this.repeat){var repeater=new Ext.util.ClickRepeater(btn,Ext.isObject(this.repeat)?this.repeat:{});this.mon(repeater,'click',this.onRepeatClick,this);}else{this.mon(btn,this.clickEvent,this.onClick,this);}},afterRender:function(){Ext.Button.superclass.afterRender.call(this);this.useSetClass=true;this.setButtonClass();this.doc=Ext.getDoc();this.doAutoWidth();},setIconClass:function(cls){this.iconCls=cls;if(this.el){this.btnEl.dom.className='';this.btnEl.addClass(['x-btn-text',cls||'']);this.setButtonClass();} +return this;},setTooltip:function(tooltip,initial){if(this.rendered){if(!initial){this.clearTip();} +if(Ext.isObject(tooltip)){Ext.QuickTips.register(Ext.apply({target:this.btnEl.id},tooltip));this.tooltip=tooltip;}else{this.btnEl.dom[this.tooltipType]=tooltip;}}else{this.tooltip=tooltip;} +return this;},clearTip:function(){if(Ext.isObject(this.tooltip)){Ext.QuickTips.unregister(this.btnEl);}},beforeDestroy:function(){if(this.rendered){this.clearTip();} +if(this.menu&&this.destroyMenu!==false){Ext.destroy(this.btnEl,this.menu);} +Ext.destroy(this.repeater);},onDestroy:function(){if(this.rendered){this.doc.un('mouseover',this.monitorMouseOver,this);this.doc.un('mouseup',this.onMouseUp,this);delete this.doc;delete this.btnEl;Ext.ButtonToggleMgr.unregister(this);} +Ext.Button.superclass.onDestroy.call(this);},doAutoWidth:function(){if(this.autoWidth!==false&&this.el&&this.text&&this.width===undefined){this.el.setWidth('auto');if(Ext.isIE7&&Ext.isStrict){var ib=this.btnEl;if(ib&&ib.getWidth()>20){ib.clip();ib.setWidth(Ext.util.TextMetrics.measure(ib,this.text).width+ib.getFrameWidth('lr'));}} +if(this.minWidth){if(this.el.getWidth()<this.minWidth){this.el.setWidth(this.minWidth);}}}},setHandler:function(handler,scope){this.handler=handler;this.scope=scope;return this;},setText:function(text){this.text=text;if(this.el){this.btnEl.update(text||' ');this.setButtonClass();} +this.doAutoWidth();return this;},setIcon:function(icon){this.icon=icon;if(this.el){this.btnEl.setStyle('background-image',icon?'url('+icon+')':'');this.setButtonClass();} +return this;},getText:function(){return this.text;},toggle:function(state,suppressEvent){state=state===undefined?!this.pressed:!!state;if(state!=this.pressed){if(this.rendered){this.el[state?'addClass':'removeClass']('x-btn-pressed');} +this.pressed=state;if(!suppressEvent){this.fireEvent('toggle',this,state);if(this.toggleHandler){this.toggleHandler.call(this.scope||this,this,state);}}} +return this;},onDisable:function(){this.onDisableChange(true);},onEnable:function(){this.onDisableChange(false);},onDisableChange:function(disabled){if(this.el){if(!Ext.isIE6||!this.text){this.el[disabled?'addClass':'removeClass'](this.disabledClass);} +this.el.dom.disabled=disabled;} +this.disabled=disabled;},showMenu:function(){if(this.rendered&&this.menu){if(this.tooltip){Ext.QuickTips.getQuickTip().cancelShow(this.btnEl);} +if(this.menu.isVisible()){this.menu.hide();} +this.menu.ownerCt=this;this.menu.show(this.el,this.menuAlign);} +return this;},hideMenu:function(){if(this.hasVisibleMenu()){this.menu.hide();} +return this;},hasVisibleMenu:function(){return this.menu&&this.menu.ownerCt==this&&this.menu.isVisible();},onRepeatClick:function(repeat,e){this.onClick(e);},onClick:function(e){if(e){e.preventDefault();} +if(e.button!==0){return;} +if(!this.disabled){this.doToggle();if(this.menu&&!this.hasVisibleMenu()&&!this.ignoreNextClick){this.showMenu();} +this.fireEvent('click',this,e);if(this.handler){this.handler.call(this.scope||this,this,e);}}},doToggle:function(){if(this.enableToggle&&(this.allowDepress!==false||!this.pressed)){this.toggle();}},isMenuTriggerOver:function(e,internal){return this.menu&&!internal;},isMenuTriggerOut:function(e,internal){return this.menu&&!internal;},onMouseOver:function(e){if(!this.disabled){var internal=e.within(this.el,true);if(!internal){this.el.addClass('x-btn-over');if(!this.monitoringMouseOver){this.doc.on('mouseover',this.monitorMouseOver,this);this.monitoringMouseOver=true;} +this.fireEvent('mouseover',this,e);} +if(this.isMenuTriggerOver(e,internal)){this.fireEvent('menutriggerover',this,this.menu,e);}}},monitorMouseOver:function(e){if(e.target!=this.el.dom&&!e.within(this.el)){if(this.monitoringMouseOver){this.doc.un('mouseover',this.monitorMouseOver,this);this.monitoringMouseOver=false;} +this.onMouseOut(e);}},onMouseOut:function(e){var internal=e.within(this.el)&&e.target!=this.el.dom;this.el.removeClass('x-btn-over');this.fireEvent('mouseout',this,e);if(this.isMenuTriggerOut(e,internal)){this.fireEvent('menutriggerout',this,this.menu,e);}},focus:function(){this.btnEl.focus();},blur:function(){this.btnEl.blur();},onFocus:function(e){if(!this.disabled){this.el.addClass('x-btn-focus');}},onBlur:function(e){this.el.removeClass('x-btn-focus');},getClickEl:function(e,isUp){return this.el;},onMouseDown:function(e){if(!this.disabled&&e.button===0){this.getClickEl(e).addClass('x-btn-click');this.doc.on('mouseup',this.onMouseUp,this);}},onMouseUp:function(e){if(e.button===0){this.getClickEl(e,true).removeClass('x-btn-click');this.doc.un('mouseup',this.onMouseUp,this);}},onMenuShow:function(e){if(this.menu.ownerCt==this){this.menu.ownerCt=this;this.ignoreNextClick=0;this.el.addClass('x-btn-menu-active');this.fireEvent('menushow',this,this.menu);}},onMenuHide:function(e){if(this.menu.ownerCt==this){this.el.removeClass('x-btn-menu-active');this.ignoreNextClick=this.restoreClick.defer(250,this);this.fireEvent('menuhide',this,this.menu);delete this.menu.ownerCt;}},restoreClick:function(){this.ignoreNextClick=0;}});Ext.reg('button',Ext.Button);Ext.ButtonToggleMgr=function(){var groups={};function toggleGroup(btn,state){if(state){var g=groups[btn.toggleGroup];for(var i=0,l=g.length;i<l;i++){if(g[i]!=btn){g[i].toggle(false);}}}} +return{register:function(btn){if(!btn.toggleGroup){return;} +var g=groups[btn.toggleGroup];if(!g){g=groups[btn.toggleGroup]=[];} +g.push(btn);btn.on('toggle',toggleGroup);},unregister:function(btn){if(!btn.toggleGroup){return;} +var g=groups[btn.toggleGroup];if(g){g.remove(btn);btn.un('toggle',toggleGroup);}},getPressed:function(group){var g=groups[group];if(g){for(var i=0,len=g.length;i<len;i++){if(g[i].pressed===true){return g[i];}}} +return null;}};}();Ext.SplitButton=Ext.extend(Ext.Button,{arrowSelector:'em',split:true,initComponent:function(){Ext.SplitButton.superclass.initComponent.call(this);this.addEvents("arrowclick");},onRender:function(){Ext.SplitButton.superclass.onRender.apply(this,arguments);if(this.arrowTooltip){this.el.child(this.arrowSelector).dom[this.tooltipType]=this.arrowTooltip;}},setArrowHandler:function(handler,scope){this.arrowHandler=handler;this.scope=scope;},getMenuClass:function(){return'x-btn-split'+(this.arrowAlign=='bottom'?'-bottom':'');},isClickOnArrow:function(e){if(this.arrowAlign!='bottom'){var visBtn=this.el.child('em.x-btn-split');var right=visBtn.getRegion().right-visBtn.getPadding('r');return e.getPageX()>right;}else{return e.getPageY()>this.btnEl.getRegion().bottom;}},onClick:function(e,t){e.preventDefault();if(!this.disabled){if(this.isClickOnArrow(e)){if(this.menu&&!this.menu.isVisible()&&!this.ignoreNextClick){this.showMenu();} +this.fireEvent("arrowclick",this,e);if(this.arrowHandler){this.arrowHandler.call(this.scope||this,this,e);}}else{this.doToggle();this.fireEvent("click",this,e);if(this.handler){this.handler.call(this.scope||this,this,e);}}}},isMenuTriggerOver:function(e){return this.menu&&e.target.tagName==this.arrowSelector;},isMenuTriggerOut:function(e,internal){return this.menu&&e.target.tagName!=this.arrowSelector;}});Ext.reg('splitbutton',Ext.SplitButton);Ext.CycleButton=Ext.extend(Ext.SplitButton,{getItemText:function(item){if(item&&this.showText===true){var text='';if(this.prependText){text+=this.prependText;} +text+=item.text;return text;} +return undefined;},setActiveItem:function(item,suppressEvent){if(!Ext.isObject(item)){item=this.menu.getComponent(item);} +if(item){if(!this.rendered){this.text=this.getItemText(item);this.iconCls=item.iconCls;}else{var t=this.getItemText(item);if(t){this.setText(t);} +this.setIconClass(item.iconCls);} +this.activeItem=item;if(!item.checked){item.setChecked(true,false);} +if(this.forceIcon){this.setIconClass(this.forceIcon);} +if(!suppressEvent){this.fireEvent('change',this,item);}}},getActiveItem:function(){return this.activeItem;},initComponent:function(){this.addEvents("change");if(this.changeHandler){this.on('change',this.changeHandler,this.scope||this);delete this.changeHandler;} +this.itemCount=this.items.length;this.menu={cls:'x-cycle-menu',items:[]};var checked=0;Ext.each(this.items,function(item,i){Ext.apply(item,{group:item.group||this.id,itemIndex:i,checkHandler:this.checkHandler,scope:this,checked:item.checked||false});this.menu.items.push(item);if(item.checked){checked=i;}},this);Ext.CycleButton.superclass.initComponent.call(this);this.on('click',this.toggleSelected,this);this.setActiveItem(checked,true);},checkHandler:function(item,pressed){if(pressed){this.setActiveItem(item);}},toggleSelected:function(){var m=this.menu;m.render();if(!m.hasLayout){m.doLayout();} +var nextIdx,checkItem;for(var i=1;i<this.itemCount;i++){nextIdx=(this.activeItem.itemIndex+i)%this.itemCount;checkItem=m.items.itemAt(nextIdx);if(!checkItem.disabled){checkItem.setChecked(true);break;}}}});Ext.reg('cycle',Ext.CycleButton);(function(){var Event=Ext.EventManager;var Dom=Ext.lib.Dom;Ext.dd.DragDrop=function(id,sGroup,config){if(id){this.init(id,sGroup,config);}};Ext.dd.DragDrop.prototype={id:null,config:null,dragElId:null,handleElId:null,invalidHandleTypes:null,invalidHandleIds:null,invalidHandleClasses:null,startPageX:0,startPageY:0,groups:null,locked:false,lock:function(){this.locked=true;},moveOnly:false,unlock:function(){this.locked=false;},isTarget:true,padding:null,_domRef:null,__ygDragDrop:true,constrainX:false,constrainY:false,minX:0,maxX:0,minY:0,maxY:0,maintainOffset:false,xTicks:null,yTicks:null,primaryButtonOnly:true,available:false,hasOuterHandles:false,b4StartDrag:function(x,y){},startDrag:function(x,y){},b4Drag:function(e){},onDrag:function(e){},onDragEnter:function(e,id){},b4DragOver:function(e){},onDragOver:function(e,id){},b4DragOut:function(e){},onDragOut:function(e,id){},b4DragDrop:function(e){},onDragDrop:function(e,id){},onInvalidDrop:function(e){},b4EndDrag:function(e){},endDrag:function(e){},b4MouseDown:function(e){},onMouseDown:function(e){},onMouseUp:function(e){},onAvailable:function(){},defaultPadding:{left:0,right:0,top:0,bottom:0},constrainTo:function(constrainTo,pad,inContent){if(Ext.isNumber(pad)){pad={left:pad,right:pad,top:pad,bottom:pad};} +pad=pad||this.defaultPadding;var b=Ext.get(this.getEl()).getBox(),ce=Ext.get(constrainTo),s=ce.getScroll(),c,cd=ce.dom;if(cd==document.body){c={x:s.left,y:s.top,width:Ext.lib.Dom.getViewWidth(),height:Ext.lib.Dom.getViewHeight()};}else{var xy=ce.getXY();c={x:xy[0],y:xy[1],width:cd.clientWidth,height:cd.clientHeight};} +var topSpace=b.y-c.y,leftSpace=b.x-c.x;this.resetConstraints();this.setXConstraint(leftSpace-(pad.left||0),c.width-leftSpace-b.width-(pad.right||0),this.xTickSize);this.setYConstraint(topSpace-(pad.top||0),c.height-topSpace-b.height-(pad.bottom||0),this.yTickSize);},getEl:function(){if(!this._domRef){this._domRef=Ext.getDom(this.id);} +return this._domRef;},getDragEl:function(){return Ext.getDom(this.dragElId);},init:function(id,sGroup,config){this.initTarget(id,sGroup,config);Event.on(this.id,"mousedown",this.handleMouseDown,this);},initTarget:function(id,sGroup,config){this.config=config||{};this.DDM=Ext.dd.DDM;this.groups={};if(typeof id!=="string"){id=Ext.id(id);} +this.id=id;this.addToGroup((sGroup)?sGroup:"default");this.handleElId=id;this.setDragElId(id);this.invalidHandleTypes={A:"A"};this.invalidHandleIds={};this.invalidHandleClasses=[];this.applyConfig();this.handleOnAvailable();},applyConfig:function(){this.padding=this.config.padding||[0,0,0,0];this.isTarget=(this.config.isTarget!==false);this.maintainOffset=(this.config.maintainOffset);this.primaryButtonOnly=(this.config.primaryButtonOnly!==false);},handleOnAvailable:function(){this.available=true;this.resetConstraints();this.onAvailable();},setPadding:function(iTop,iRight,iBot,iLeft){if(!iRight&&0!==iRight){this.padding=[iTop,iTop,iTop,iTop];}else if(!iBot&&0!==iBot){this.padding=[iTop,iRight,iTop,iRight];}else{this.padding=[iTop,iRight,iBot,iLeft];}},setInitPosition:function(diffX,diffY){var el=this.getEl();if(!this.DDM.verifyEl(el)){return;} +var dx=diffX||0;var dy=diffY||0;var p=Dom.getXY(el);this.initPageX=p[0]-dx;this.initPageY=p[1]-dy;this.lastPageX=p[0];this.lastPageY=p[1];this.setStartPosition(p);},setStartPosition:function(pos){var p=pos||Dom.getXY(this.getEl());this.deltaSetXY=null;this.startPageX=p[0];this.startPageY=p[1];},addToGroup:function(sGroup){this.groups[sGroup]=true;this.DDM.regDragDrop(this,sGroup);},removeFromGroup:function(sGroup){if(this.groups[sGroup]){delete this.groups[sGroup];} +this.DDM.removeDDFromGroup(this,sGroup);},setDragElId:function(id){this.dragElId=id;},setHandleElId:function(id){if(typeof id!=="string"){id=Ext.id(id);} +this.handleElId=id;this.DDM.regHandle(this.id,id);},setOuterHandleElId:function(id){if(typeof id!=="string"){id=Ext.id(id);} +Event.on(id,"mousedown",this.handleMouseDown,this);this.setHandleElId(id);this.hasOuterHandles=true;},unreg:function(){Event.un(this.id,"mousedown",this.handleMouseDown);this._domRef=null;this.DDM._remove(this);},destroy:function(){this.unreg();},isLocked:function(){return(this.DDM.isLocked()||this.locked);},handleMouseDown:function(e,oDD){if(this.primaryButtonOnly&&e.button!=0){return;} +if(this.isLocked()){return;} +this.DDM.refreshCache(this.groups);var pt=new Ext.lib.Point(Ext.lib.Event.getPageX(e),Ext.lib.Event.getPageY(e));if(!this.hasOuterHandles&&!this.DDM.isOverTarget(pt,this)){}else{if(this.clickValidator(e)){this.setStartPosition();this.b4MouseDown(e);this.onMouseDown(e);this.DDM.handleMouseDown(e,this);this.DDM.stopEvent(e);}else{}}},clickValidator:function(e){var target=e.getTarget();return(this.isValidHandleChild(target)&&(this.id==this.handleElId||this.DDM.handleWasClicked(target,this.id)));},addInvalidHandleType:function(tagName){var type=tagName.toUpperCase();this.invalidHandleTypes[type]=type;},addInvalidHandleId:function(id){if(typeof id!=="string"){id=Ext.id(id);} +this.invalidHandleIds[id]=id;},addInvalidHandleClass:function(cssClass){this.invalidHandleClasses.push(cssClass);},removeInvalidHandleType:function(tagName){var type=tagName.toUpperCase();delete this.invalidHandleTypes[type];},removeInvalidHandleId:function(id){if(typeof id!=="string"){id=Ext.id(id);} +delete this.invalidHandleIds[id];},removeInvalidHandleClass:function(cssClass){for(var i=0,len=this.invalidHandleClasses.length;i<len;++i){if(this.invalidHandleClasses[i]==cssClass){delete this.invalidHandleClasses[i];}}},isValidHandleChild:function(node){var valid=true;var nodeName;try{nodeName=node.nodeName.toUpperCase();}catch(e){nodeName=node.nodeName;} +valid=valid&&!this.invalidHandleTypes[nodeName];valid=valid&&!this.invalidHandleIds[node.id];for(var i=0,len=this.invalidHandleClasses.length;valid&&i<len;++i){valid=!Ext.fly(node).hasClass(this.invalidHandleClasses[i]);} +return valid;},setXTicks:function(iStartX,iTickSize){this.xTicks=[];this.xTickSize=iTickSize;var tickMap={};for(var i=this.initPageX;i>=this.minX;i=i-iTickSize){if(!tickMap[i]){this.xTicks[this.xTicks.length]=i;tickMap[i]=true;}} +for(i=this.initPageX;i<=this.maxX;i=i+iTickSize){if(!tickMap[i]){this.xTicks[this.xTicks.length]=i;tickMap[i]=true;}} +this.xTicks.sort(this.DDM.numericSort);},setYTicks:function(iStartY,iTickSize){this.yTicks=[];this.yTickSize=iTickSize;var tickMap={};for(var i=this.initPageY;i>=this.minY;i=i-iTickSize){if(!tickMap[i]){this.yTicks[this.yTicks.length]=i;tickMap[i]=true;}} +for(i=this.initPageY;i<=this.maxY;i=i+iTickSize){if(!tickMap[i]){this.yTicks[this.yTicks.length]=i;tickMap[i]=true;}} +this.yTicks.sort(this.DDM.numericSort);},setXConstraint:function(iLeft,iRight,iTickSize){this.leftConstraint=iLeft;this.rightConstraint=iRight;this.minX=this.initPageX-iLeft;this.maxX=this.initPageX+iRight;if(iTickSize){this.setXTicks(this.initPageX,iTickSize);} +this.constrainX=true;},clearConstraints:function(){this.constrainX=false;this.constrainY=false;this.clearTicks();},clearTicks:function(){this.xTicks=null;this.yTicks=null;this.xTickSize=0;this.yTickSize=0;},setYConstraint:function(iUp,iDown,iTickSize){this.topConstraint=iUp;this.bottomConstraint=iDown;this.minY=this.initPageY-iUp;this.maxY=this.initPageY+iDown;if(iTickSize){this.setYTicks(this.initPageY,iTickSize);} +this.constrainY=true;},resetConstraints:function(){if(this.initPageX||this.initPageX===0){var dx=(this.maintainOffset)?this.lastPageX-this.initPageX:0;var dy=(this.maintainOffset)?this.lastPageY-this.initPageY:0;this.setInitPosition(dx,dy);}else{this.setInitPosition();} +if(this.constrainX){this.setXConstraint(this.leftConstraint,this.rightConstraint,this.xTickSize);} +if(this.constrainY){this.setYConstraint(this.topConstraint,this.bottomConstraint,this.yTickSize);}},getTick:function(val,tickArray){if(!tickArray){return val;}else if(tickArray[0]>=val){return tickArray[0];}else{for(var i=0,len=tickArray.length;i<len;++i){var next=i+1;if(tickArray[next]&&tickArray[next]>=val){var diff1=val-tickArray[i];var diff2=tickArray[next]-val;return(diff2>diff1)?tickArray[i]:tickArray[next];}} +return tickArray[tickArray.length-1];}},toString:function(){return("DragDrop "+this.id);}};})();if(!Ext.dd.DragDropMgr){Ext.dd.DragDropMgr=function(){var Event=Ext.EventManager;return{ids:{},handleIds:{},dragCurrent:null,dragOvers:{},deltaX:0,deltaY:0,preventDefault:true,stopPropagation:true,initialized:false,locked:false,init:function(){this.initialized=true;},POINT:0,INTERSECT:1,mode:0,_execOnAll:function(sMethod,args){for(var i in this.ids){for(var j in this.ids[i]){var oDD=this.ids[i][j];if(!this.isTypeOfDD(oDD)){continue;} +oDD[sMethod].apply(oDD,args);}}},_onLoad:function(){this.init();Event.on(document,"mouseup",this.handleMouseUp,this,true);Event.on(document,"mousemove",this.handleMouseMove,this,true);Event.on(window,"unload",this._onUnload,this,true);Event.on(window,"resize",this._onResize,this,true);},_onResize:function(e){this._execOnAll("resetConstraints",[]);},lock:function(){this.locked=true;},unlock:function(){this.locked=false;},isLocked:function(){return this.locked;},locationCache:{},useCache:true,clickPixelThresh:3,clickTimeThresh:350,dragThreshMet:false,clickTimeout:null,startX:0,startY:0,regDragDrop:function(oDD,sGroup){if(!this.initialized){this.init();} +if(!this.ids[sGroup]){this.ids[sGroup]={};} +this.ids[sGroup][oDD.id]=oDD;},removeDDFromGroup:function(oDD,sGroup){if(!this.ids[sGroup]){this.ids[sGroup]={};} +var obj=this.ids[sGroup];if(obj&&obj[oDD.id]){delete obj[oDD.id];}},_remove:function(oDD){for(var g in oDD.groups){if(g&&this.ids[g]&&this.ids[g][oDD.id]){delete this.ids[g][oDD.id];}} +delete this.handleIds[oDD.id];},regHandle:function(sDDId,sHandleId){if(!this.handleIds[sDDId]){this.handleIds[sDDId]={};} +this.handleIds[sDDId][sHandleId]=sHandleId;},isDragDrop:function(id){return(this.getDDById(id))?true:false;},getRelated:function(p_oDD,bTargetsOnly){var oDDs=[];for(var i in p_oDD.groups){for(var j in this.ids[i]){var dd=this.ids[i][j];if(!this.isTypeOfDD(dd)){continue;} +if(!bTargetsOnly||dd.isTarget){oDDs[oDDs.length]=dd;}}} +return oDDs;},isLegalTarget:function(oDD,oTargetDD){var targets=this.getRelated(oDD,true);for(var i=0,len=targets.length;i<len;++i){if(targets[i].id==oTargetDD.id){return true;}} +return false;},isTypeOfDD:function(oDD){return(oDD&&oDD.__ygDragDrop);},isHandle:function(sDDId,sHandleId){return(this.handleIds[sDDId]&&this.handleIds[sDDId][sHandleId]);},getDDById:function(id){for(var i in this.ids){if(this.ids[i][id]){return this.ids[i][id];}} +return null;},handleMouseDown:function(e,oDD){if(Ext.QuickTips){Ext.QuickTips.ddDisable();} +if(this.dragCurrent){this.handleMouseUp(e);} +this.currentTarget=e.getTarget();this.dragCurrent=oDD;var el=oDD.getEl();this.startX=e.getPageX();this.startY=e.getPageY();this.deltaX=this.startX-el.offsetLeft;this.deltaY=this.startY-el.offsetTop;this.dragThreshMet=false;this.clickTimeout=setTimeout(function(){var DDM=Ext.dd.DDM;DDM.startDrag(DDM.startX,DDM.startY);},this.clickTimeThresh);},startDrag:function(x,y){clearTimeout(this.clickTimeout);if(this.dragCurrent){this.dragCurrent.b4StartDrag(x,y);this.dragCurrent.startDrag(x,y);} +this.dragThreshMet=true;},handleMouseUp:function(e){if(Ext.QuickTips){Ext.QuickTips.ddEnable();} +if(!this.dragCurrent){return;} +clearTimeout(this.clickTimeout);if(this.dragThreshMet){this.fireEvents(e,true);}else{} +this.stopDrag(e);this.stopEvent(e);},stopEvent:function(e){if(this.stopPropagation){e.stopPropagation();} +if(this.preventDefault){e.preventDefault();}},stopDrag:function(e){if(this.dragCurrent){if(this.dragThreshMet){this.dragCurrent.b4EndDrag(e);this.dragCurrent.endDrag(e);} +this.dragCurrent.onMouseUp(e);} +this.dragCurrent=null;this.dragOvers={};},handleMouseMove:function(e){if(!this.dragCurrent){return true;} +if(Ext.isIE&&(e.button!==0&&e.button!==1&&e.button!==2)){this.stopEvent(e);return this.handleMouseUp(e);} +if(!this.dragThreshMet){var diffX=Math.abs(this.startX-e.getPageX());var diffY=Math.abs(this.startY-e.getPageY());if(diffX>this.clickPixelThresh||diffY>this.clickPixelThresh){this.startDrag(this.startX,this.startY);}} +if(this.dragThreshMet){this.dragCurrent.b4Drag(e);this.dragCurrent.onDrag(e);if(!this.dragCurrent.moveOnly){this.fireEvents(e,false);}} +this.stopEvent(e);return true;},fireEvents:function(e,isDrop){var dc=this.dragCurrent;if(!dc||dc.isLocked()){return;} +var pt=e.getPoint();var oldOvers=[];var outEvts=[];var overEvts=[];var dropEvts=[];var enterEvts=[];for(var i in this.dragOvers){var ddo=this.dragOvers[i];if(!this.isTypeOfDD(ddo)){continue;} +if(!this.isOverTarget(pt,ddo,this.mode)){outEvts.push(ddo);} +oldOvers[i]=true;delete this.dragOvers[i];} +for(var sGroup in dc.groups){if("string"!=typeof sGroup){continue;} +for(i in this.ids[sGroup]){var oDD=this.ids[sGroup][i];if(!this.isTypeOfDD(oDD)){continue;} +if(oDD.isTarget&&!oDD.isLocked()&&((oDD!=dc)||(dc.ignoreSelf===false))){if(this.isOverTarget(pt,oDD,this.mode)){if(isDrop){dropEvts.push(oDD);}else{if(!oldOvers[oDD.id]){enterEvts.push(oDD);}else{overEvts.push(oDD);} +this.dragOvers[oDD.id]=oDD;}}}}} +if(this.mode){if(outEvts.length){dc.b4DragOut(e,outEvts);dc.onDragOut(e,outEvts);} +if(enterEvts.length){dc.onDragEnter(e,enterEvts);} +if(overEvts.length){dc.b4DragOver(e,overEvts);dc.onDragOver(e,overEvts);} +if(dropEvts.length){dc.b4DragDrop(e,dropEvts);dc.onDragDrop(e,dropEvts);}}else{var len=0;for(i=0,len=outEvts.length;i<len;++i){dc.b4DragOut(e,outEvts[i].id);dc.onDragOut(e,outEvts[i].id);} +for(i=0,len=enterEvts.length;i<len;++i){dc.onDragEnter(e,enterEvts[i].id);} +for(i=0,len=overEvts.length;i<len;++i){dc.b4DragOver(e,overEvts[i].id);dc.onDragOver(e,overEvts[i].id);} +for(i=0,len=dropEvts.length;i<len;++i){dc.b4DragDrop(e,dropEvts[i].id);dc.onDragDrop(e,dropEvts[i].id);}} +if(isDrop&&!dropEvts.length){dc.onInvalidDrop(e);}},getBestMatch:function(dds){var winner=null;var len=dds.length;if(len==1){winner=dds[0];}else{for(var i=0;i<len;++i){var dd=dds[i];if(dd.cursorIsOver){winner=dd;break;}else{if(!winner||winner.overlap.getArea()<dd.overlap.getArea()){winner=dd;}}}} +return winner;},refreshCache:function(groups){for(var sGroup in groups){if("string"!=typeof sGroup){continue;} +for(var i in this.ids[sGroup]){var oDD=this.ids[sGroup][i];if(this.isTypeOfDD(oDD)){var loc=this.getLocation(oDD);if(loc){this.locationCache[oDD.id]=loc;}else{delete this.locationCache[oDD.id];}}}}},verifyEl:function(el){if(el){var parent;if(Ext.isIE){try{parent=el.offsetParent;}catch(e){}}else{parent=el.offsetParent;} +if(parent){return true;}} +return false;},getLocation:function(oDD){if(!this.isTypeOfDD(oDD)){return null;} +var el=oDD.getEl(),pos,x1,x2,y1,y2,t,r,b,l;try{pos=Ext.lib.Dom.getXY(el);}catch(e){} +if(!pos){return null;} +x1=pos[0];x2=x1+el.offsetWidth;y1=pos[1];y2=y1+el.offsetHeight;t=y1-oDD.padding[0];r=x2+oDD.padding[1];b=y2+oDD.padding[2];l=x1-oDD.padding[3];return new Ext.lib.Region(t,r,b,l);},isOverTarget:function(pt,oTarget,intersect){var loc=this.locationCache[oTarget.id];if(!loc||!this.useCache){loc=this.getLocation(oTarget);this.locationCache[oTarget.id]=loc;} +if(!loc){return false;} +oTarget.cursorIsOver=loc.contains(pt);var dc=this.dragCurrent;if(!dc||!dc.getTargetCoord||(!intersect&&!dc.constrainX&&!dc.constrainY)){return oTarget.cursorIsOver;} +oTarget.overlap=null;var pos=dc.getTargetCoord(pt.x,pt.y);var el=dc.getDragEl();var curRegion=new Ext.lib.Region(pos.y,pos.x+el.offsetWidth,pos.y+el.offsetHeight,pos.x);var overlap=curRegion.intersect(loc);if(overlap){oTarget.overlap=overlap;return(intersect)?true:oTarget.cursorIsOver;}else{return false;}},_onUnload:function(e,me){Ext.dd.DragDropMgr.unregAll();},unregAll:function(){if(this.dragCurrent){this.stopDrag();this.dragCurrent=null;} +this._execOnAll("unreg",[]);for(var i in this.elementCache){delete this.elementCache[i];} +this.elementCache={};this.ids={};},elementCache:{},getElWrapper:function(id){var oWrapper=this.elementCache[id];if(!oWrapper||!oWrapper.el){oWrapper=this.elementCache[id]=new this.ElementWrapper(Ext.getDom(id));} +return oWrapper;},getElement:function(id){return Ext.getDom(id);},getCss:function(id){var el=Ext.getDom(id);return(el)?el.style:null;},ElementWrapper:function(el){this.el=el||null;this.id=this.el&&el.id;this.css=this.el&&el.style;},getPosX:function(el){return Ext.lib.Dom.getX(el);},getPosY:function(el){return Ext.lib.Dom.getY(el);},swapNode:function(n1,n2){if(n1.swapNode){n1.swapNode(n2);}else{var p=n2.parentNode;var s=n2.nextSibling;if(s==n1){p.insertBefore(n1,n2);}else if(n2==n1.nextSibling){p.insertBefore(n2,n1);}else{n1.parentNode.replaceChild(n2,n1);p.insertBefore(n1,s);}}},getScroll:function(){var t,l,dde=document.documentElement,db=document.body;if(dde&&(dde.scrollTop||dde.scrollLeft)){t=dde.scrollTop;l=dde.scrollLeft;}else if(db){t=db.scrollTop;l=db.scrollLeft;}else{} +return{top:t,left:l};},getStyle:function(el,styleProp){return Ext.fly(el).getStyle(styleProp);},getScrollTop:function(){return this.getScroll().top;},getScrollLeft:function(){return this.getScroll().left;},moveToEl:function(moveEl,targetEl){var aCoord=Ext.lib.Dom.getXY(targetEl);Ext.lib.Dom.setXY(moveEl,aCoord);},numericSort:function(a,b){return(a-b);},_timeoutCount:0,_addListeners:function(){var DDM=Ext.dd.DDM;if(Ext.lib.Event&&document){DDM._onLoad();}else{if(DDM._timeoutCount>2000){}else{setTimeout(DDM._addListeners,10);if(document&&document.body){DDM._timeoutCount+=1;}}}},handleWasClicked:function(node,id){if(this.isHandle(id,node.id)){return true;}else{var p=node.parentNode;while(p){if(this.isHandle(id,p.id)){return true;}else{p=p.parentNode;}}} +return false;}};}();Ext.dd.DDM=Ext.dd.DragDropMgr;Ext.dd.DDM._addListeners();} +Ext.dd.DD=function(id,sGroup,config){if(id){this.init(id,sGroup,config);}};Ext.extend(Ext.dd.DD,Ext.dd.DragDrop,{scroll:true,autoOffset:function(iPageX,iPageY){var x=iPageX-this.startPageX;var y=iPageY-this.startPageY;this.setDelta(x,y);},setDelta:function(iDeltaX,iDeltaY){this.deltaX=iDeltaX;this.deltaY=iDeltaY;},setDragElPos:function(iPageX,iPageY){var el=this.getDragEl();this.alignElWithMouse(el,iPageX,iPageY);},alignElWithMouse:function(el,iPageX,iPageY){var oCoord=this.getTargetCoord(iPageX,iPageY);var fly=el.dom?el:Ext.fly(el,'_dd');if(!this.deltaSetXY){var aCoord=[oCoord.x,oCoord.y];fly.setXY(aCoord);var newLeft=fly.getLeft(true);var newTop=fly.getTop(true);this.deltaSetXY=[newLeft-oCoord.x,newTop-oCoord.y];}else{fly.setLeftTop(oCoord.x+this.deltaSetXY[0],oCoord.y+this.deltaSetXY[1]);} +this.cachePosition(oCoord.x,oCoord.y);this.autoScroll(oCoord.x,oCoord.y,el.offsetHeight,el.offsetWidth);return oCoord;},cachePosition:function(iPageX,iPageY){if(iPageX){this.lastPageX=iPageX;this.lastPageY=iPageY;}else{var aCoord=Ext.lib.Dom.getXY(this.getEl());this.lastPageX=aCoord[0];this.lastPageY=aCoord[1];}},autoScroll:function(x,y,h,w){if(this.scroll){var clientH=Ext.lib.Dom.getViewHeight();var clientW=Ext.lib.Dom.getViewWidth();var st=this.DDM.getScrollTop();var sl=this.DDM.getScrollLeft();var bot=h+y;var right=w+x;var toBot=(clientH+st-y-this.deltaY);var toRight=(clientW+sl-x-this.deltaX);var thresh=40;var scrAmt=(document.all)?80:30;if(bot>clientH&&toBot<thresh){window.scrollTo(sl,st+scrAmt);} +if(y<st&&st>0&&y-st<thresh){window.scrollTo(sl,st-scrAmt);} +if(right>clientW&&toRight<thresh){window.scrollTo(sl+scrAmt,st);} +if(x<sl&&sl>0&&x-sl<thresh){window.scrollTo(sl-scrAmt,st);}}},getTargetCoord:function(iPageX,iPageY){var x=iPageX-this.deltaX;var y=iPageY-this.deltaY;if(this.constrainX){if(x<this.minX){x=this.minX;} +if(x>this.maxX){x=this.maxX;}} +if(this.constrainY){if(y<this.minY){y=this.minY;} +if(y>this.maxY){y=this.maxY;}} +x=this.getTick(x,this.xTicks);y=this.getTick(y,this.yTicks);return{x:x,y:y};},applyConfig:function(){Ext.dd.DD.superclass.applyConfig.call(this);this.scroll=(this.config.scroll!==false);},b4MouseDown:function(e){this.autoOffset(e.getPageX(),e.getPageY());},b4Drag:function(e){this.setDragElPos(e.getPageX(),e.getPageY());},toString:function(){return("DD "+this.id);}});Ext.dd.DDProxy=function(id,sGroup,config){if(id){this.init(id,sGroup,config);this.initFrame();}};Ext.dd.DDProxy.dragElId="ygddfdiv";Ext.extend(Ext.dd.DDProxy,Ext.dd.DD,{resizeFrame:true,centerFrame:false,createFrame:function(){var self=this;var body=document.body;if(!body||!body.firstChild){setTimeout(function(){self.createFrame();},50);return;} +var div=this.getDragEl();if(!div){div=document.createElement("div");div.id=this.dragElId;var s=div.style;s.position="absolute";s.visibility="hidden";s.cursor="move";s.border="2px solid #aaa";s.zIndex=999;body.insertBefore(div,body.firstChild);}},initFrame:function(){this.createFrame();},applyConfig:function(){Ext.dd.DDProxy.superclass.applyConfig.call(this);this.resizeFrame=(this.config.resizeFrame!==false);this.centerFrame=(this.config.centerFrame);this.setDragElId(this.config.dragElId||Ext.dd.DDProxy.dragElId);},showFrame:function(iPageX,iPageY){var el=this.getEl();var dragEl=this.getDragEl();var s=dragEl.style;this._resizeProxy();if(this.centerFrame){this.setDelta(Math.round(parseInt(s.width,10)/2),Math.round(parseInt(s.height,10)/2));} +this.setDragElPos(iPageX,iPageY);Ext.fly(dragEl).show();},_resizeProxy:function(){if(this.resizeFrame){var el=this.getEl();Ext.fly(this.getDragEl()).setSize(el.offsetWidth,el.offsetHeight);}},b4MouseDown:function(e){var x=e.getPageX();var y=e.getPageY();this.autoOffset(x,y);this.setDragElPos(x,y);},b4StartDrag:function(x,y){this.showFrame(x,y);},b4EndDrag:function(e){Ext.fly(this.getDragEl()).hide();},endDrag:function(e){var lel=this.getEl();var del=this.getDragEl();del.style.visibility="";this.beforeMove();lel.style.visibility="hidden";Ext.dd.DDM.moveToEl(lel,del);del.style.visibility="hidden";lel.style.visibility="";this.afterDrag();},beforeMove:function(){},afterDrag:function(){},toString:function(){return("DDProxy "+this.id);}});Ext.dd.DDTarget=function(id,sGroup,config){if(id){this.initTarget(id,sGroup,config);}};Ext.extend(Ext.dd.DDTarget,Ext.dd.DragDrop,{getDragEl:Ext.emptyFn,isValidHandleChild:Ext.emptyFn,startDrag:Ext.emptyFn,endDrag:Ext.emptyFn,onDrag:Ext.emptyFn,onDragDrop:Ext.emptyFn,onDragEnter:Ext.emptyFn,onDragOut:Ext.emptyFn,onDragOver:Ext.emptyFn,onInvalidDrop:Ext.emptyFn,onMouseDown:Ext.emptyFn,onMouseUp:Ext.emptyFn,setXConstraint:Ext.emptyFn,setYConstraint:Ext.emptyFn,resetConstraints:Ext.emptyFn,clearConstraints:Ext.emptyFn,clearTicks:Ext.emptyFn,setInitPosition:Ext.emptyFn,setDragElId:Ext.emptyFn,setHandleElId:Ext.emptyFn,setOuterHandleElId:Ext.emptyFn,addInvalidHandleClass:Ext.emptyFn,addInvalidHandleId:Ext.emptyFn,addInvalidHandleType:Ext.emptyFn,removeInvalidHandleClass:Ext.emptyFn,removeInvalidHandleId:Ext.emptyFn,removeInvalidHandleType:Ext.emptyFn,toString:function(){return("DDTarget "+this.id);}});Ext.dd.DragTracker=Ext.extend(Ext.util.Observable,{active:false,tolerance:5,autoStart:false,constructor:function(config){Ext.apply(this,config);this.addEvents('mousedown','mouseup','mousemove','dragstart','dragend','drag');this.dragRegion=new Ext.lib.Region(0,0,0,0);if(this.el){this.initEl(this.el);} +Ext.dd.DragTracker.superclass.constructor.call(this,config);},initEl:function(el){this.el=Ext.get(el);el.on('mousedown',this.onMouseDown,this,this.delegate?{delegate:this.delegate}:undefined);},destroy:function(){this.el.un('mousedown',this.onMouseDown,this);delete this.el;},onMouseDown:function(e,target){if(this.fireEvent('mousedown',this,e)!==false&&this.onBeforeStart(e)!==false){this.startXY=this.lastXY=e.getXY();this.dragTarget=this.delegate?target:this.el.dom;if(this.preventDefault!==false){e.preventDefault();} +Ext.getDoc().on({scope:this,mouseup:this.onMouseUp,mousemove:this.onMouseMove,selectstart:this.stopSelect});if(this.autoStart){this.timer=this.triggerStart.defer(this.autoStart===true?1000:this.autoStart,this,[e]);}}},onMouseMove:function(e,target){if(this.active&&Ext.isIE&&!e.browserEvent.button){e.preventDefault();this.onMouseUp(e);return;} +e.preventDefault();var xy=e.getXY(),s=this.startXY;this.lastXY=xy;if(!this.active){if(Math.abs(s[0]-xy[0])>this.tolerance||Math.abs(s[1]-xy[1])>this.tolerance){this.triggerStart(e);}else{return;}} +this.fireEvent('mousemove',this,e);this.onDrag(e);this.fireEvent('drag',this,e);},onMouseUp:function(e){var doc=Ext.getDoc(),wasActive=this.active;doc.un('mousemove',this.onMouseMove,this);doc.un('mouseup',this.onMouseUp,this);doc.un('selectstart',this.stopSelect,this);e.preventDefault();this.clearStart();this.active=false;delete this.elRegion;this.fireEvent('mouseup',this,e);if(wasActive){this.onEnd(e);this.fireEvent('dragend',this,e);}},triggerStart:function(e){this.clearStart();this.active=true;this.onStart(e);this.fireEvent('dragstart',this,e);},clearStart:function(){if(this.timer){clearTimeout(this.timer);delete this.timer;}},stopSelect:function(e){e.stopEvent();return false;},onBeforeStart:function(e){},onStart:function(xy){},onDrag:function(e){},onEnd:function(e){},getDragTarget:function(){return this.dragTarget;},getDragCt:function(){return this.el;},getXY:function(constrain){return constrain?this.constrainModes[constrain].call(this,this.lastXY):this.lastXY;},getOffset:function(constrain){var xy=this.getXY(constrain),s=this.startXY;return[s[0]-xy[0],s[1]-xy[1]];},constrainModes:{'point':function(xy){if(!this.elRegion){this.elRegion=this.getDragCt().getRegion();} +var dr=this.dragRegion;dr.left=xy[0];dr.top=xy[1];dr.right=xy[0];dr.bottom=xy[1];dr.constrainTo(this.elRegion);return[dr.left,dr.top];}}});Ext.dd.ScrollManager=function(){var ddm=Ext.dd.DragDropMgr;var els={};var dragEl=null;var proc={};var onStop=function(e){dragEl=null;clearProc();};var triggerRefresh=function(){if(ddm.dragCurrent){ddm.refreshCache(ddm.dragCurrent.groups);}};var doScroll=function(){if(ddm.dragCurrent){var dds=Ext.dd.ScrollManager;var inc=proc.el.ddScrollConfig?proc.el.ddScrollConfig.increment:dds.increment;if(!dds.animate){if(proc.el.scroll(proc.dir,inc)){triggerRefresh();}}else{proc.el.scroll(proc.dir,inc,true,dds.animDuration,triggerRefresh);}}};var clearProc=function(){if(proc.id){clearInterval(proc.id);} +proc.id=0;proc.el=null;proc.dir="";};var startProc=function(el,dir){clearProc();proc.el=el;proc.dir=dir;var group=el.ddScrollConfig?el.ddScrollConfig.ddGroup:undefined,freq=(el.ddScrollConfig&&el.ddScrollConfig.frequency)?el.ddScrollConfig.frequency:Ext.dd.ScrollManager.frequency;if(group===undefined||ddm.dragCurrent.ddGroup==group){proc.id=setInterval(doScroll,freq);}};var onFire=function(e,isDrop){if(isDrop||!ddm.dragCurrent){return;} +var dds=Ext.dd.ScrollManager;if(!dragEl||dragEl!=ddm.dragCurrent){dragEl=ddm.dragCurrent;dds.refreshCache();} +var xy=Ext.lib.Event.getXY(e);var pt=new Ext.lib.Point(xy[0],xy[1]);for(var id in els){var el=els[id],r=el._region;var c=el.ddScrollConfig?el.ddScrollConfig:dds;if(r&&r.contains(pt)&&el.isScrollable()){if(r.bottom-pt.y<=c.vthresh){if(proc.el!=el){startProc(el,"down");} +return;}else if(r.right-pt.x<=c.hthresh){if(proc.el!=el){startProc(el,"left");} +return;}else if(pt.y-r.top<=c.vthresh){if(proc.el!=el){startProc(el,"up");} +return;}else if(pt.x-r.left<=c.hthresh){if(proc.el!=el){startProc(el,"right");} +return;}}} +clearProc();};ddm.fireEvents=ddm.fireEvents.createSequence(onFire,ddm);ddm.stopDrag=ddm.stopDrag.createSequence(onStop,ddm);return{register:function(el){if(Ext.isArray(el)){for(var i=0,len=el.length;i<len;i++){this.register(el[i]);}}else{el=Ext.get(el);els[el.id]=el;}},unregister:function(el){if(Ext.isArray(el)){for(var i=0,len=el.length;i<len;i++){this.unregister(el[i]);}}else{el=Ext.get(el);delete els[el.id];}},vthresh:25,hthresh:25,increment:100,frequency:500,animate:true,animDuration:.4,ddGroup:undefined,refreshCache:function(){for(var id in els){if(typeof els[id]=='object'){els[id]._region=els[id].getRegion();}}}};}();Ext.dd.Registry=function(){var elements={};var handles={};var autoIdSeed=0;var getId=function(el,autogen){if(typeof el=="string"){return el;} +var id=el.id;if(!id&&autogen!==false){id="extdd-"+(++autoIdSeed);el.id=id;} +return id;};return{register:function(el,data){data=data||{};if(typeof el=="string"){el=document.getElementById(el);} +data.ddel=el;elements[getId(el)]=data;if(data.isHandle!==false){handles[data.ddel.id]=data;} +if(data.handles){var hs=data.handles;for(var i=0,len=hs.length;i<len;i++){handles[getId(hs[i])]=data;}}},unregister:function(el){var id=getId(el,false);var data=elements[id];if(data){delete elements[id];if(data.handles){var hs=data.handles;for(var i=0,len=hs.length;i<len;i++){delete handles[getId(hs[i],false)];}}}},getHandle:function(id){if(typeof id!="string"){id=id.id;} +return handles[id];},getHandleFromEvent:function(e){var t=Ext.lib.Event.getTarget(e);return t?handles[t.id]:null;},getTarget:function(id){if(typeof id!="string"){id=id.id;} +return elements[id];},getTargetFromEvent:function(e){var t=Ext.lib.Event.getTarget(e);return t?elements[t.id]||handles[t.id]:null;}};}();Ext.dd.StatusProxy=function(config){Ext.apply(this,config);this.id=this.id||Ext.id();this.el=new Ext.Layer({dh:{id:this.id,tag:"div",cls:"x-dd-drag-proxy "+this.dropNotAllowed,children:[{tag:"div",cls:"x-dd-drop-icon"},{tag:"div",cls:"x-dd-drag-ghost"}]},shadow:!config||config.shadow!==false});this.ghost=Ext.get(this.el.dom.childNodes[1]);this.dropStatus=this.dropNotAllowed;};Ext.dd.StatusProxy.prototype={dropAllowed:"x-dd-drop-ok",dropNotAllowed:"x-dd-drop-nodrop",setStatus:function(cssClass){cssClass=cssClass||this.dropNotAllowed;if(this.dropStatus!=cssClass){this.el.replaceClass(this.dropStatus,cssClass);this.dropStatus=cssClass;}},reset:function(clearGhost){this.el.dom.className="x-dd-drag-proxy "+this.dropNotAllowed;this.dropStatus=this.dropNotAllowed;if(clearGhost){this.ghost.update("");}},update:function(html){if(typeof html=="string"){this.ghost.update(html);}else{this.ghost.update("");html.style.margin="0";this.ghost.dom.appendChild(html);} +var el=this.ghost.dom.firstChild;if(el){Ext.fly(el).setStyle('float','none');}},getEl:function(){return this.el;},getGhost:function(){return this.ghost;},hide:function(clear){this.el.hide();if(clear){this.reset(true);}},stop:function(){if(this.anim&&this.anim.isAnimated&&this.anim.isAnimated()){this.anim.stop();}},show:function(){this.el.show();},sync:function(){this.el.sync();},repair:function(xy,callback,scope){this.callback=callback;this.scope=scope;if(xy&&this.animRepair!==false){this.el.addClass("x-dd-drag-repair");this.el.hideUnders(true);this.anim=this.el.shift({duration:this.repairDuration||.5,easing:'easeOut',xy:xy,stopFx:true,callback:this.afterRepair,scope:this});}else{this.afterRepair();}},afterRepair:function(){this.hide(true);if(typeof this.callback=="function"){this.callback.call(this.scope||this);} +this.callback=null;this.scope=null;},destroy:function(){Ext.destroy(this.ghost,this.el);}};Ext.dd.DragSource=function(el,config){this.el=Ext.get(el);if(!this.dragData){this.dragData={};} +Ext.apply(this,config);if(!this.proxy){this.proxy=new Ext.dd.StatusProxy();} +Ext.dd.DragSource.superclass.constructor.call(this,this.el.dom,this.ddGroup||this.group,{dragElId:this.proxy.id,resizeFrame:false,isTarget:false,scroll:this.scroll===true});this.dragging=false;};Ext.extend(Ext.dd.DragSource,Ext.dd.DDProxy,{dropAllowed:"x-dd-drop-ok",dropNotAllowed:"x-dd-drop-nodrop",getDragData:function(e){return this.dragData;},onDragEnter:function(e,id){var target=Ext.dd.DragDropMgr.getDDById(id);this.cachedTarget=target;if(this.beforeDragEnter(target,e,id)!==false){if(target.isNotifyTarget){var status=target.notifyEnter(this,e,this.dragData);this.proxy.setStatus(status);}else{this.proxy.setStatus(this.dropAllowed);} +if(this.afterDragEnter){this.afterDragEnter(target,e,id);}}},beforeDragEnter:function(target,e,id){return true;},alignElWithMouse:function(){Ext.dd.DragSource.superclass.alignElWithMouse.apply(this,arguments);this.proxy.sync();},onDragOver:function(e,id){var target=this.cachedTarget||Ext.dd.DragDropMgr.getDDById(id);if(this.beforeDragOver(target,e,id)!==false){if(target.isNotifyTarget){var status=target.notifyOver(this,e,this.dragData);this.proxy.setStatus(status);} +if(this.afterDragOver){this.afterDragOver(target,e,id);}}},beforeDragOver:function(target,e,id){return true;},onDragOut:function(e,id){var target=this.cachedTarget||Ext.dd.DragDropMgr.getDDById(id);if(this.beforeDragOut(target,e,id)!==false){if(target.isNotifyTarget){target.notifyOut(this,e,this.dragData);} +this.proxy.reset();if(this.afterDragOut){this.afterDragOut(target,e,id);}} +this.cachedTarget=null;},beforeDragOut:function(target,e,id){return true;},onDragDrop:function(e,id){var target=this.cachedTarget||Ext.dd.DragDropMgr.getDDById(id);if(this.beforeDragDrop(target,e,id)!==false){if(target.isNotifyTarget){if(target.notifyDrop(this,e,this.dragData)){this.onValidDrop(target,e,id);}else{this.onInvalidDrop(target,e,id);}}else{this.onValidDrop(target,e,id);} +if(this.afterDragDrop){this.afterDragDrop(target,e,id);}} +delete this.cachedTarget;},beforeDragDrop:function(target,e,id){return true;},onValidDrop:function(target,e,id){this.hideProxy();if(this.afterValidDrop){this.afterValidDrop(target,e,id);}},getRepairXY:function(e,data){return this.el.getXY();},onInvalidDrop:function(target,e,id){this.beforeInvalidDrop(target,e,id);if(this.cachedTarget){if(this.cachedTarget.isNotifyTarget){this.cachedTarget.notifyOut(this,e,this.dragData);} +this.cacheTarget=null;} +this.proxy.repair(this.getRepairXY(e,this.dragData),this.afterRepair,this);if(this.afterInvalidDrop){this.afterInvalidDrop(e,id);}},afterRepair:function(){if(Ext.enableFx){this.el.highlight(this.hlColor||"c3daf9");} +this.dragging=false;},beforeInvalidDrop:function(target,e,id){return true;},handleMouseDown:function(e){if(this.dragging){return;} +var data=this.getDragData(e);if(data&&this.onBeforeDrag(data,e)!==false){this.dragData=data;this.proxy.stop();Ext.dd.DragSource.superclass.handleMouseDown.apply(this,arguments);}},onBeforeDrag:function(data,e){return true;},onStartDrag:Ext.emptyFn,startDrag:function(x,y){this.proxy.reset();this.dragging=true;this.proxy.update("");this.onInitDrag(x,y);this.proxy.show();},onInitDrag:function(x,y){var clone=this.el.dom.cloneNode(true);clone.id=Ext.id();this.proxy.update(clone);this.onStartDrag(x,y);return true;},getProxy:function(){return this.proxy;},hideProxy:function(){this.proxy.hide();this.proxy.reset(true);this.dragging=false;},triggerCacheRefresh:function(){Ext.dd.DDM.refreshCache(this.groups);},b4EndDrag:function(e){},endDrag:function(e){this.onEndDrag(this.dragData,e);},onEndDrag:function(data,e){},autoOffset:function(x,y){this.setDelta(-12,-20);},destroy:function(){Ext.dd.DragSource.superclass.destroy.call(this);Ext.destroy(this.proxy);}});Ext.dd.DropTarget=Ext.extend(Ext.dd.DDTarget,{constructor:function(el,config){this.el=Ext.get(el);Ext.apply(this,config);if(this.containerScroll){Ext.dd.ScrollManager.register(this.el);} +Ext.dd.DropTarget.superclass.constructor.call(this,this.el.dom,this.ddGroup||this.group,{isTarget:true});},dropAllowed:"x-dd-drop-ok",dropNotAllowed:"x-dd-drop-nodrop",isTarget:true,isNotifyTarget:true,notifyEnter:function(dd,e,data){if(this.overClass){this.el.addClass(this.overClass);} +return this.dropAllowed;},notifyOver:function(dd,e,data){return this.dropAllowed;},notifyOut:function(dd,e,data){if(this.overClass){this.el.removeClass(this.overClass);}},notifyDrop:function(dd,e,data){return false;},destroy:function(){Ext.dd.DropTarget.superclass.destroy.call(this);if(this.containerScroll){Ext.dd.ScrollManager.unregister(this.el);}}});Ext.dd.DragZone=Ext.extend(Ext.dd.DragSource,{constructor:function(el,config){Ext.dd.DragZone.superclass.constructor.call(this,el,config);if(this.containerScroll){Ext.dd.ScrollManager.register(this.el);}},getDragData:function(e){return Ext.dd.Registry.getHandleFromEvent(e);},onInitDrag:function(x,y){this.proxy.update(this.dragData.ddel.cloneNode(true));this.onStartDrag(x,y);return true;},afterRepair:function(){if(Ext.enableFx){Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor||"c3daf9");} +this.dragging=false;},getRepairXY:function(e){return Ext.Element.fly(this.dragData.ddel).getXY();},destroy:function(){Ext.dd.DragZone.superclass.destroy.call(this);if(this.containerScroll){Ext.dd.ScrollManager.unregister(this.el);}}});Ext.dd.DropZone=function(el,config){Ext.dd.DropZone.superclass.constructor.call(this,el,config);};Ext.extend(Ext.dd.DropZone,Ext.dd.DropTarget,{getTargetFromEvent:function(e){return Ext.dd.Registry.getTargetFromEvent(e);},onNodeEnter:function(n,dd,e,data){},onNodeOver:function(n,dd,e,data){return this.dropAllowed;},onNodeOut:function(n,dd,e,data){},onNodeDrop:function(n,dd,e,data){return false;},onContainerOver:function(dd,e,data){return this.dropNotAllowed;},onContainerDrop:function(dd,e,data){return false;},notifyEnter:function(dd,e,data){return this.dropNotAllowed;},notifyOver:function(dd,e,data){var n=this.getTargetFromEvent(e);if(!n){if(this.lastOverNode){this.onNodeOut(this.lastOverNode,dd,e,data);this.lastOverNode=null;} +return this.onContainerOver(dd,e,data);} +if(this.lastOverNode!=n){if(this.lastOverNode){this.onNodeOut(this.lastOverNode,dd,e,data);} +this.onNodeEnter(n,dd,e,data);this.lastOverNode=n;} +return this.onNodeOver(n,dd,e,data);},notifyOut:function(dd,e,data){if(this.lastOverNode){this.onNodeOut(this.lastOverNode,dd,e,data);this.lastOverNode=null;}},notifyDrop:function(dd,e,data){if(this.lastOverNode){this.onNodeOut(this.lastOverNode,dd,e,data);this.lastOverNode=null;} +var n=this.getTargetFromEvent(e);return n?this.onNodeDrop(n,dd,e,data):this.onContainerDrop(dd,e,data);},triggerCacheRefresh:function(){Ext.dd.DDM.refreshCache(this.groups);}});Ext.Element.addMethods({initDD:function(group,config,overrides){var dd=new Ext.dd.DD(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides);},initDDProxy:function(group,config,overrides){var dd=new Ext.dd.DDProxy(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides);},initDDTarget:function(group,config,overrides){var dd=new Ext.dd.DDTarget(Ext.id(this.dom),group,config);return Ext.apply(dd,overrides);}});Ext.tree.TreePanel=Ext.extend(Ext.Panel,{rootVisible:true,animate:Ext.enableFx,lines:true,enableDD:false,hlDrop:Ext.enableFx,pathSeparator:'/',bubbleEvents:[],initComponent:function(){Ext.tree.TreePanel.superclass.initComponent.call(this);if(!this.eventModel){this.eventModel=new Ext.tree.TreeEventModel(this);} +var l=this.loader;if(!l){l=new Ext.tree.TreeLoader({dataUrl:this.dataUrl,requestMethod:this.requestMethod});}else if(Ext.isObject(l)&&!l.load){l=new Ext.tree.TreeLoader(l);} +this.loader=l;this.nodeHash={};if(this.root){var r=this.root;delete this.root;this.setRootNode(r);} +this.addEvents('append','remove','movenode','insert','beforeappend','beforeremove','beforemovenode','beforeinsert','beforeload','load','textchange','beforeexpandnode','beforecollapsenode','expandnode','disabledchange','collapsenode','beforeclick','click','containerclick','checkchange','beforedblclick','dblclick','containerdblclick','contextmenu','containercontextmenu','beforechildrenrendered','startdrag','enddrag','dragdrop','beforenodedrop','nodedrop','nodedragover');if(this.singleExpand){this.on('beforeexpandnode',this.restrictExpand,this);}},proxyNodeEvent:function(ename,a1,a2,a3,a4,a5,a6){if(ename=='collapse'||ename=='expand'||ename=='beforecollapse'||ename=='beforeexpand'||ename=='move'||ename=='beforemove'){ename=ename+'node';} +return this.fireEvent(ename,a1,a2,a3,a4,a5,a6);},getRootNode:function(){return this.root;},setRootNode:function(node){this.destroyRoot();if(!node.render){node=this.loader.createNode(node);} +this.root=node;node.ownerTree=this;node.isRoot=true;this.registerNode(node);if(!this.rootVisible){var uiP=node.attributes.uiProvider;node.ui=uiP?new uiP(node):new Ext.tree.RootTreeNodeUI(node);} +if(this.innerCt){this.clearInnerCt();this.renderRoot();} +return node;},clearInnerCt:function(){this.innerCt.update('');},renderRoot:function(){this.root.render();if(!this.rootVisible){this.root.renderChildren();}},getNodeById:function(id){return this.nodeHash[id];},registerNode:function(node){this.nodeHash[node.id]=node;},unregisterNode:function(node){delete this.nodeHash[node.id];},toString:function(){return'[Tree'+(this.id?' '+this.id:'')+']';},restrictExpand:function(node){var p=node.parentNode;if(p){if(p.expandedChild&&p.expandedChild.parentNode==p){p.expandedChild.collapse();} +p.expandedChild=node;}},getChecked:function(a,startNode){startNode=startNode||this.root;var r=[];var f=function(){if(this.attributes.checked){r.push(!a?this:(a=='id'?this.id:this.attributes[a]));}};startNode.cascade(f);return r;},getLoader:function(){return this.loader;},expandAll:function(){this.root.expand(true);},collapseAll:function(){this.root.collapse(true);},getSelectionModel:function(){if(!this.selModel){this.selModel=new Ext.tree.DefaultSelectionModel();} +return this.selModel;},expandPath:function(path,attr,callback){if(Ext.isEmpty(path)){if(callback){callback(false,undefined);} +return;} +attr=attr||'id';var keys=path.split(this.pathSeparator);var curNode=this.root;if(curNode.attributes[attr]!=keys[1]){if(callback){callback(false,null);} +return;} +var index=1;var f=function(){if(++index==keys.length){if(callback){callback(true,curNode);} +return;} +var c=curNode.findChild(attr,keys[index]);if(!c){if(callback){callback(false,curNode);} +return;} +curNode=c;c.expand(false,false,f);};curNode.expand(false,false,f);},selectPath:function(path,attr,callback){if(Ext.isEmpty(path)){if(callback){callback(false,undefined);} +return;} +attr=attr||'id';var keys=path.split(this.pathSeparator),v=keys.pop();if(keys.length>1){var f=function(success,node){if(success&&node){var n=node.findChild(attr,v);if(n){n.select();if(callback){callback(true,n);}}else if(callback){callback(false,n);}}else{if(callback){callback(false,n);}}};this.expandPath(keys.join(this.pathSeparator),attr,f);}else{this.root.select();if(callback){callback(true,this.root);}}},getTreeEl:function(){return this.body;},onRender:function(ct,position){Ext.tree.TreePanel.superclass.onRender.call(this,ct,position);this.el.addClass('x-tree');this.innerCt=this.body.createChild({tag:'ul',cls:'x-tree-root-ct '+ +(this.useArrows?'x-tree-arrows':this.lines?'x-tree-lines':'x-tree-no-lines')});},initEvents:function(){Ext.tree.TreePanel.superclass.initEvents.call(this);if(this.containerScroll){Ext.dd.ScrollManager.register(this.body);} +if((this.enableDD||this.enableDrop)&&!this.dropZone){this.dropZone=new Ext.tree.TreeDropZone(this,this.dropConfig||{ddGroup:this.ddGroup||'TreeDD',appendOnly:this.ddAppendOnly===true});} +if((this.enableDD||this.enableDrag)&&!this.dragZone){this.dragZone=new Ext.tree.TreeDragZone(this,this.dragConfig||{ddGroup:this.ddGroup||'TreeDD',scroll:this.ddScroll});} +this.getSelectionModel().init(this);},afterRender:function(){Ext.tree.TreePanel.superclass.afterRender.call(this);this.renderRoot();},beforeDestroy:function(){if(this.rendered){Ext.dd.ScrollManager.unregister(this.body);Ext.destroy(this.dropZone,this.dragZone);} +this.destroyRoot();Ext.destroy(this.loader);this.nodeHash=this.root=this.loader=null;Ext.tree.TreePanel.superclass.beforeDestroy.call(this);},destroyRoot:function(){if(this.root&&this.root.destroy){this.root.destroy(true);}}});Ext.tree.TreePanel.nodeTypes={};Ext.reg('treepanel',Ext.tree.TreePanel);Ext.tree.TreeEventModel=function(tree){this.tree=tree;this.tree.on('render',this.initEvents,this);};Ext.tree.TreeEventModel.prototype={initEvents:function(){var t=this.tree;if(t.trackMouseOver!==false){t.mon(t.innerCt,{scope:this,mouseover:this.delegateOver,mouseout:this.delegateOut});} +t.mon(t.getTreeEl(),{scope:this,click:this.delegateClick,dblclick:this.delegateDblClick,contextmenu:this.delegateContextMenu});},getNode:function(e){var t;if(t=e.getTarget('.x-tree-node-el',10)){var id=Ext.fly(t,'_treeEvents').getAttribute('tree-node-id','ext');if(id){return this.tree.getNodeById(id);}} +return null;},getNodeTarget:function(e){var t=e.getTarget('.x-tree-node-icon',1);if(!t){t=e.getTarget('.x-tree-node-el',6);} +return t;},delegateOut:function(e,t){if(!this.beforeEvent(e)){return;} +if(e.getTarget('.x-tree-ec-icon',1)){var n=this.getNode(e);this.onIconOut(e,n);if(n==this.lastEcOver){delete this.lastEcOver;}} +if((t=this.getNodeTarget(e))&&!e.within(t,true)){this.onNodeOut(e,this.getNode(e));}},delegateOver:function(e,t){if(!this.beforeEvent(e)){return;} +if(Ext.isGecko&&!this.trackingDoc){Ext.getBody().on('mouseover',this.trackExit,this);this.trackingDoc=true;} +if(this.lastEcOver){this.onIconOut(e,this.lastEcOver);delete this.lastEcOver;} +if(e.getTarget('.x-tree-ec-icon',1)){this.lastEcOver=this.getNode(e);this.onIconOver(e,this.lastEcOver);} +if(t=this.getNodeTarget(e)){this.onNodeOver(e,this.getNode(e));}},trackExit:function(e){if(this.lastOverNode){if(this.lastOverNode.ui&&!e.within(this.lastOverNode.ui.getEl())){this.onNodeOut(e,this.lastOverNode);} +delete this.lastOverNode;Ext.getBody().un('mouseover',this.trackExit,this);this.trackingDoc=false;}},delegateClick:function(e,t){if(this.beforeEvent(e)){if(e.getTarget('input[type=checkbox]',1)){this.onCheckboxClick(e,this.getNode(e));}else if(e.getTarget('.x-tree-ec-icon',1)){this.onIconClick(e,this.getNode(e));}else if(this.getNodeTarget(e)){this.onNodeClick(e,this.getNode(e));}}else{this.checkContainerEvent(e,'click');}},delegateDblClick:function(e,t){if(this.beforeEvent(e)){if(this.getNodeTarget(e)){this.onNodeDblClick(e,this.getNode(e));}}else{this.checkContainerEvent(e,'dblclick');}},delegateContextMenu:function(e,t){if(this.beforeEvent(e)){if(this.getNodeTarget(e)){this.onNodeContextMenu(e,this.getNode(e));}}else{this.checkContainerEvent(e,'contextmenu');}},checkContainerEvent:function(e,type){if(this.disabled){e.stopEvent();return false;} +this.onContainerEvent(e,type);},onContainerEvent:function(e,type){this.tree.fireEvent('container'+type,this.tree,e);},onNodeClick:function(e,node){node.ui.onClick(e);},onNodeOver:function(e,node){this.lastOverNode=node;node.ui.onOver(e);},onNodeOut:function(e,node){node.ui.onOut(e);},onIconOver:function(e,node){node.ui.addClass('x-tree-ec-over');},onIconOut:function(e,node){node.ui.removeClass('x-tree-ec-over');},onIconClick:function(e,node){node.ui.ecClick(e);},onCheckboxClick:function(e,node){node.ui.onCheckChange(e);},onNodeDblClick:function(e,node){node.ui.onDblClick(e);},onNodeContextMenu:function(e,node){node.ui.onContextMenu(e);},beforeEvent:function(e){var node=this.getNode(e);if(this.disabled||!node||!node.ui){e.stopEvent();return false;} +return true;},disable:function(){this.disabled=true;},enable:function(){this.disabled=false;}};Ext.tree.DefaultSelectionModel=Ext.extend(Ext.util.Observable,{constructor:function(config){this.selNode=null;this.addEvents('selectionchange','beforeselect');Ext.apply(this,config);Ext.tree.DefaultSelectionModel.superclass.constructor.call(this);},init:function(tree){this.tree=tree;tree.mon(tree.getTreeEl(),'keydown',this.onKeyDown,this);tree.on('click',this.onNodeClick,this);},onNodeClick:function(node,e){this.select(node);},select:function(node,selectNextNode){if(!Ext.fly(node.ui.wrap).isVisible()&&selectNextNode){return selectNextNode.call(this,node);} +var last=this.selNode;if(node==last){node.ui.onSelectedChange(true);}else if(this.fireEvent('beforeselect',this,node,last)!==false){if(last&&last.ui){last.ui.onSelectedChange(false);} +this.selNode=node;node.ui.onSelectedChange(true);this.fireEvent('selectionchange',this,node,last);} +return node;},unselect:function(node,silent){if(this.selNode==node){this.clearSelections(silent);}},clearSelections:function(silent){var n=this.selNode;if(n){n.ui.onSelectedChange(false);this.selNode=null;if(silent!==true){this.fireEvent('selectionchange',this,null);}} +return n;},getSelectedNode:function(){return this.selNode;},isSelected:function(node){return this.selNode==node;},selectPrevious:function(s){if(!(s=s||this.selNode||this.lastSelNode)){return null;} +var ps=s.previousSibling;if(ps){if(!ps.isExpanded()||ps.childNodes.length<1){return this.select(ps,this.selectPrevious);}else{var lc=ps.lastChild;while(lc&&lc.isExpanded()&&Ext.fly(lc.ui.wrap).isVisible()&&lc.childNodes.length>0){lc=lc.lastChild;} +return this.select(lc,this.selectPrevious);}}else if(s.parentNode&&(this.tree.rootVisible||!s.parentNode.isRoot)){return this.select(s.parentNode,this.selectPrevious);} +return null;},selectNext:function(s){if(!(s=s||this.selNode||this.lastSelNode)){return null;} +if(s.firstChild&&s.isExpanded()&&Ext.fly(s.ui.wrap).isVisible()){return this.select(s.firstChild,this.selectNext);}else if(s.nextSibling){return this.select(s.nextSibling,this.selectNext);}else if(s.parentNode){var newS=null;s.parentNode.bubble(function(){if(this.nextSibling){newS=this.getOwnerTree().selModel.select(this.nextSibling,this.selectNext);return false;}});return newS;} +return null;},onKeyDown:function(e){var s=this.selNode||this.lastSelNode;var sm=this;if(!s){return;} +var k=e.getKey();switch(k){case e.DOWN:e.stopEvent();this.selectNext();break;case e.UP:e.stopEvent();this.selectPrevious();break;case e.RIGHT:e.preventDefault();if(s.hasChildNodes()){if(!s.isExpanded()){s.expand();}else if(s.firstChild){this.select(s.firstChild,e);}} +break;case e.LEFT:e.preventDefault();if(s.hasChildNodes()&&s.isExpanded()){s.collapse();}else if(s.parentNode&&(this.tree.rootVisible||s.parentNode!=this.tree.getRootNode())){this.select(s.parentNode,e);} +break;};}});Ext.tree.MultiSelectionModel=Ext.extend(Ext.util.Observable,{constructor:function(config){this.selNodes=[];this.selMap={};this.addEvents('selectionchange');Ext.apply(this,config);Ext.tree.MultiSelectionModel.superclass.constructor.call(this);},init:function(tree){this.tree=tree;tree.mon(tree.getTreeEl(),'keydown',this.onKeyDown,this);tree.on('click',this.onNodeClick,this);},onNodeClick:function(node,e){if(e.ctrlKey&&this.isSelected(node)){this.unselect(node);}else{this.select(node,e,e.ctrlKey);}},select:function(node,e,keepExisting){if(keepExisting!==true){this.clearSelections(true);} +if(this.isSelected(node)){this.lastSelNode=node;return node;} +this.selNodes.push(node);this.selMap[node.id]=node;this.lastSelNode=node;node.ui.onSelectedChange(true);this.fireEvent('selectionchange',this,this.selNodes);return node;},unselect:function(node){if(this.selMap[node.id]){node.ui.onSelectedChange(false);var sn=this.selNodes;var index=sn.indexOf(node);if(index!=-1){this.selNodes.splice(index,1);} +delete this.selMap[node.id];this.fireEvent('selectionchange',this,this.selNodes);}},clearSelections:function(suppressEvent){var sn=this.selNodes;if(sn.length>0){for(var i=0,len=sn.length;i<len;i++){sn[i].ui.onSelectedChange(false);} +this.selNodes=[];this.selMap={};if(suppressEvent!==true){this.fireEvent('selectionchange',this,this.selNodes);}}},isSelected:function(node){return this.selMap[node.id]?true:false;},getSelectedNodes:function(){return this.selNodes.concat([]);},onKeyDown:Ext.tree.DefaultSelectionModel.prototype.onKeyDown,selectNext:Ext.tree.DefaultSelectionModel.prototype.selectNext,selectPrevious:Ext.tree.DefaultSelectionModel.prototype.selectPrevious});Ext.data.Tree=Ext.extend(Ext.util.Observable,{constructor:function(root){this.nodeHash={};this.root=null;if(root){this.setRootNode(root);} +this.addEvents("append","remove","move","insert","beforeappend","beforeremove","beforemove","beforeinsert");Ext.data.Tree.superclass.constructor.call(this);},pathSeparator:"/",proxyNodeEvent:function(){return this.fireEvent.apply(this,arguments);},getRootNode:function(){return this.root;},setRootNode:function(node){this.root=node;node.ownerTree=this;node.isRoot=true;this.registerNode(node);return node;},getNodeById:function(id){return this.nodeHash[id];},registerNode:function(node){this.nodeHash[node.id]=node;},unregisterNode:function(node){delete this.nodeHash[node.id];},toString:function(){return"[Tree"+(this.id?" "+this.id:"")+"]";}});Ext.data.Node=Ext.extend(Ext.util.Observable,{constructor:function(attributes){this.attributes=attributes||{};this.leaf=this.attributes.leaf;this.id=this.attributes.id;if(!this.id){this.id=Ext.id(null,"xnode-");this.attributes.id=this.id;} +this.childNodes=[];this.parentNode=null;this.firstChild=null;this.lastChild=null;this.previousSibling=null;this.nextSibling=null;this.addEvents({"append":true,"remove":true,"move":true,"insert":true,"beforeappend":true,"beforeremove":true,"beforemove":true,"beforeinsert":true});this.listeners=this.attributes.listeners;Ext.data.Node.superclass.constructor.call(this);},fireEvent:function(evtName){if(Ext.data.Node.superclass.fireEvent.apply(this,arguments)===false){return false;} +var ot=this.getOwnerTree();if(ot){if(ot.proxyNodeEvent.apply(ot,arguments)===false){return false;}} +return true;},isLeaf:function(){return this.leaf===true;},setFirstChild:function(node){this.firstChild=node;},setLastChild:function(node){this.lastChild=node;},isLast:function(){return(!this.parentNode?true:this.parentNode.lastChild==this);},isFirst:function(){return(!this.parentNode?true:this.parentNode.firstChild==this);},hasChildNodes:function(){return!this.isLeaf()&&this.childNodes.length>0;},isExpandable:function(){return this.attributes.expandable||this.hasChildNodes();},appendChild:function(node){var multi=false;if(Ext.isArray(node)){multi=node;}else if(arguments.length>1){multi=arguments;} +if(multi){for(var i=0,len=multi.length;i<len;i++){this.appendChild(multi[i]);}}else{if(this.fireEvent("beforeappend",this.ownerTree,this,node)===false){return false;} +var index=this.childNodes.length;var oldParent=node.parentNode;if(oldParent){if(node.fireEvent("beforemove",node.getOwnerTree(),node,oldParent,this,index)===false){return false;} +oldParent.removeChild(node);} +index=this.childNodes.length;if(index===0){this.setFirstChild(node);} +this.childNodes.push(node);node.parentNode=this;var ps=this.childNodes[index-1];if(ps){node.previousSibling=ps;ps.nextSibling=node;}else{node.previousSibling=null;} +node.nextSibling=null;this.setLastChild(node);node.setOwnerTree(this.getOwnerTree());this.fireEvent("append",this.ownerTree,this,node,index);if(oldParent){node.fireEvent("move",this.ownerTree,node,oldParent,this,index);} +return node;}},removeChild:function(node,destroy){var index=this.childNodes.indexOf(node);if(index==-1){return false;} +if(this.fireEvent("beforeremove",this.ownerTree,this,node)===false){return false;} +this.childNodes.splice(index,1);if(node.previousSibling){node.previousSibling.nextSibling=node.nextSibling;} +if(node.nextSibling){node.nextSibling.previousSibling=node.previousSibling;} +if(this.firstChild==node){this.setFirstChild(node.nextSibling);} +if(this.lastChild==node){this.setLastChild(node.previousSibling);} +this.fireEvent("remove",this.ownerTree,this,node);if(destroy){node.destroy(true);}else{node.clear();} +return node;},clear:function(destroy){this.setOwnerTree(null,destroy);this.parentNode=this.previousSibling=this.nextSibling=null;if(destroy){this.firstChild=this.lastChild=null;}},destroy:function(silent){if(silent===true){this.purgeListeners();this.clear(true);Ext.each(this.childNodes,function(n){n.destroy(true);});this.childNodes=null;}else{this.remove(true);}},insertBefore:function(node,refNode){if(!refNode){return this.appendChild(node);} +if(node==refNode){return false;} +if(this.fireEvent("beforeinsert",this.ownerTree,this,node,refNode)===false){return false;} +var index=this.childNodes.indexOf(refNode);var oldParent=node.parentNode;var refIndex=index;if(oldParent==this&&this.childNodes.indexOf(node)<index){refIndex--;} +if(oldParent){if(node.fireEvent("beforemove",node.getOwnerTree(),node,oldParent,this,index,refNode)===false){return false;} +oldParent.removeChild(node);} +if(refIndex===0){this.setFirstChild(node);} +this.childNodes.splice(refIndex,0,node);node.parentNode=this;var ps=this.childNodes[refIndex-1];if(ps){node.previousSibling=ps;ps.nextSibling=node;}else{node.previousSibling=null;} +node.nextSibling=refNode;refNode.previousSibling=node;node.setOwnerTree(this.getOwnerTree());this.fireEvent("insert",this.ownerTree,this,node,refNode);if(oldParent){node.fireEvent("move",this.ownerTree,node,oldParent,this,refIndex,refNode);} +return node;},remove:function(destroy){if(this.parentNode){this.parentNode.removeChild(this,destroy);} +return this;},removeAll:function(destroy){var cn=this.childNodes,n;while((n=cn[0])){this.removeChild(n,destroy);} +return this;},item:function(index){return this.childNodes[index];},replaceChild:function(newChild,oldChild){var s=oldChild?oldChild.nextSibling:null;this.removeChild(oldChild);this.insertBefore(newChild,s);return oldChild;},indexOf:function(child){return this.childNodes.indexOf(child);},getOwnerTree:function(){if(!this.ownerTree){var p=this;while(p){if(p.ownerTree){this.ownerTree=p.ownerTree;break;} +p=p.parentNode;}} +return this.ownerTree;},getDepth:function(){var depth=0;var p=this;while(p.parentNode){++depth;p=p.parentNode;} +return depth;},setOwnerTree:function(tree,destroy){if(tree!=this.ownerTree){if(this.ownerTree){this.ownerTree.unregisterNode(this);} +this.ownerTree=tree;if(destroy!==true){Ext.each(this.childNodes,function(n){n.setOwnerTree(tree);});} +if(tree){tree.registerNode(this);}}},setId:function(id){if(id!==this.id){var t=this.ownerTree;if(t){t.unregisterNode(this);} +this.id=this.attributes.id=id;if(t){t.registerNode(this);} +this.onIdChange(id);}},onIdChange:Ext.emptyFn,getPath:function(attr){attr=attr||"id";var p=this.parentNode;var b=[this.attributes[attr]];while(p){b.unshift(p.attributes[attr]);p=p.parentNode;} +var sep=this.getOwnerTree().pathSeparator;return sep+b.join(sep);},bubble:function(fn,scope,args){var p=this;while(p){if(fn.apply(scope||p,args||[p])===false){break;} +p=p.parentNode;}},cascade:function(fn,scope,args){if(fn.apply(scope||this,args||[this])!==false){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].cascade(fn,scope,args);}}},eachChild:function(fn,scope,args){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){if(fn.apply(scope||cs[i],args||[cs[i]])===false){break;}}},findChild:function(attribute,value,deep){return this.findChildBy(function(){return this.attributes[attribute]==value;},null,deep);},findChildBy:function(fn,scope,deep){var cs=this.childNodes,len=cs.length,i=0,n,res;for(;i<len;i++){n=cs[i];if(fn.call(scope||n,n)===true){return n;}else if(deep){res=n.findChildBy(fn,scope,deep);if(res!=null){return res;}}} +return null;},sort:function(fn,scope){var cs=this.childNodes;var len=cs.length;if(len>0){var sortFn=scope?function(){fn.apply(scope,arguments);}:fn;cs.sort(sortFn);for(var i=0;i<len;i++){var n=cs[i];n.previousSibling=cs[i-1];n.nextSibling=cs[i+1];if(i===0){this.setFirstChild(n);} +if(i==len-1){this.setLastChild(n);}}}},contains:function(node){return node.isAncestor(this);},isAncestor:function(node){var p=this.parentNode;while(p){if(p==node){return true;} +p=p.parentNode;} +return false;},toString:function(){return"[Node"+(this.id?" "+this.id:"")+"]";}});Ext.tree.TreeNode=Ext.extend(Ext.data.Node,{constructor:function(attributes){attributes=attributes||{};if(Ext.isString(attributes)){attributes={text:attributes};} +this.childrenRendered=false;this.rendered=false;Ext.tree.TreeNode.superclass.constructor.call(this,attributes);this.expanded=attributes.expanded===true;this.isTarget=attributes.isTarget!==false;this.draggable=attributes.draggable!==false&&attributes.allowDrag!==false;this.allowChildren=attributes.allowChildren!==false&&attributes.allowDrop!==false;this.text=attributes.text;this.disabled=attributes.disabled===true;this.hidden=attributes.hidden===true;this.addEvents('textchange','beforeexpand','beforecollapse','expand','disabledchange','collapse','beforeclick','click','checkchange','beforedblclick','dblclick','contextmenu','beforechildrenrendered');var uiClass=this.attributes.uiProvider||this.defaultUI||Ext.tree.TreeNodeUI;this.ui=new uiClass(this);},preventHScroll:true,isExpanded:function(){return this.expanded;},getUI:function(){return this.ui;},getLoader:function(){var owner;return this.loader||((owner=this.getOwnerTree())&&owner.loader?owner.loader:(this.loader=new Ext.tree.TreeLoader()));},setFirstChild:function(node){var of=this.firstChild;Ext.tree.TreeNode.superclass.setFirstChild.call(this,node);if(this.childrenRendered&&of&&node!=of){of.renderIndent(true,true);} +if(this.rendered){this.renderIndent(true,true);}},setLastChild:function(node){var ol=this.lastChild;Ext.tree.TreeNode.superclass.setLastChild.call(this,node);if(this.childrenRendered&&ol&&node!=ol){ol.renderIndent(true,true);} +if(this.rendered){this.renderIndent(true,true);}},appendChild:function(n){if(!n.render&&!Ext.isArray(n)){n=this.getLoader().createNode(n);} +var node=Ext.tree.TreeNode.superclass.appendChild.call(this,n);if(node&&this.childrenRendered){node.render();} +this.ui.updateExpandIcon();return node;},removeChild:function(node,destroy){this.ownerTree.getSelectionModel().unselect(node);Ext.tree.TreeNode.superclass.removeChild.apply(this,arguments);if(!destroy){var rendered=node.ui.rendered;if(rendered){node.ui.remove();} +if(rendered&&this.childNodes.length<1){this.collapse(false,false);}else{this.ui.updateExpandIcon();} +if(!this.firstChild&&!this.isHiddenRoot()){this.childrenRendered=false;}} +return node;},insertBefore:function(node,refNode){if(!node.render){node=this.getLoader().createNode(node);} +var newNode=Ext.tree.TreeNode.superclass.insertBefore.call(this,node,refNode);if(newNode&&refNode&&this.childrenRendered){node.render();} +this.ui.updateExpandIcon();return newNode;},setText:function(text){var oldText=this.text;this.text=this.attributes.text=text;if(this.rendered){this.ui.onTextChange(this,text,oldText);} +this.fireEvent('textchange',this,text,oldText);},setIconCls:function(cls){var old=this.attributes.iconCls;this.attributes.iconCls=cls;if(this.rendered){this.ui.onIconClsChange(this,cls,old);}},setTooltip:function(tip,title){this.attributes.qtip=tip;this.attributes.qtipTitle=title;if(this.rendered){this.ui.onTipChange(this,tip,title);}},setIcon:function(icon){this.attributes.icon=icon;if(this.rendered){this.ui.onIconChange(this,icon);}},setHref:function(href,target){this.attributes.href=href;this.attributes.hrefTarget=target;if(this.rendered){this.ui.onHrefChange(this,href,target);}},setCls:function(cls){var old=this.attributes.cls;this.attributes.cls=cls;if(this.rendered){this.ui.onClsChange(this,cls,old);}},select:function(){var t=this.getOwnerTree();if(t){t.getSelectionModel().select(this);}},unselect:function(silent){var t=this.getOwnerTree();if(t){t.getSelectionModel().unselect(this,silent);}},isSelected:function(){var t=this.getOwnerTree();return t?t.getSelectionModel().isSelected(this):false;},expand:function(deep,anim,callback,scope){if(!this.expanded){if(this.fireEvent('beforeexpand',this,deep,anim)===false){return;} +if(!this.childrenRendered){this.renderChildren();} +this.expanded=true;if(!this.isHiddenRoot()&&(this.getOwnerTree().animate&&anim!==false)||anim){this.ui.animExpand(function(){this.fireEvent('expand',this);this.runCallback(callback,scope||this,[this]);if(deep===true){this.expandChildNodes(true,true);}}.createDelegate(this));return;}else{this.ui.expand();this.fireEvent('expand',this);this.runCallback(callback,scope||this,[this]);}}else{this.runCallback(callback,scope||this,[this]);} +if(deep===true){this.expandChildNodes(true);}},runCallback:function(cb,scope,args){if(Ext.isFunction(cb)){cb.apply(scope,args);}},isHiddenRoot:function(){return this.isRoot&&!this.getOwnerTree().rootVisible;},collapse:function(deep,anim,callback,scope){if(this.expanded&&!this.isHiddenRoot()){if(this.fireEvent('beforecollapse',this,deep,anim)===false){return;} +this.expanded=false;if((this.getOwnerTree().animate&&anim!==false)||anim){this.ui.animCollapse(function(){this.fireEvent('collapse',this);this.runCallback(callback,scope||this,[this]);if(deep===true){this.collapseChildNodes(true);}}.createDelegate(this));return;}else{this.ui.collapse();this.fireEvent('collapse',this);this.runCallback(callback,scope||this,[this]);}}else if(!this.expanded){this.runCallback(callback,scope||this,[this]);} +if(deep===true){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].collapse(true,false);}}},delayedExpand:function(delay){if(!this.expandProcId){this.expandProcId=this.expand.defer(delay,this);}},cancelExpand:function(){if(this.expandProcId){clearTimeout(this.expandProcId);} +this.expandProcId=false;},toggle:function(){if(this.expanded){this.collapse();}else{this.expand();}},ensureVisible:function(callback,scope){var tree=this.getOwnerTree();tree.expandPath(this.parentNode?this.parentNode.getPath():this.getPath(),false,function(){var node=tree.getNodeById(this.id);tree.getTreeEl().scrollChildIntoView(node.ui.anchor);this.runCallback(callback,scope||this,[this]);}.createDelegate(this));},expandChildNodes:function(deep,anim){var cs=this.childNodes,i,len=cs.length;for(i=0;i<len;i++){cs[i].expand(deep,anim);}},collapseChildNodes:function(deep){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].collapse(deep);}},disable:function(){this.disabled=true;this.unselect();if(this.rendered&&this.ui.onDisableChange){this.ui.onDisableChange(this,true);} +this.fireEvent('disabledchange',this,true);},enable:function(){this.disabled=false;if(this.rendered&&this.ui.onDisableChange){this.ui.onDisableChange(this,false);} +this.fireEvent('disabledchange',this,false);},renderChildren:function(suppressEvent){if(suppressEvent!==false){this.fireEvent('beforechildrenrendered',this);} +var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].render(true);} +this.childrenRendered=true;},sort:function(fn,scope){Ext.tree.TreeNode.superclass.sort.apply(this,arguments);if(this.childrenRendered){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].render(true);}}},render:function(bulkRender){this.ui.render(bulkRender);if(!this.rendered){this.getOwnerTree().registerNode(this);this.rendered=true;if(this.expanded){this.expanded=false;this.expand(false,false);}}},renderIndent:function(deep,refresh){if(refresh){this.ui.childIndent=null;} +this.ui.renderIndent();if(deep===true&&this.childrenRendered){var cs=this.childNodes;for(var i=0,len=cs.length;i<len;i++){cs[i].renderIndent(true,refresh);}}},beginUpdate:function(){this.childrenRendered=false;},endUpdate:function(){if(this.expanded&&this.rendered){this.renderChildren();}},destroy:function(silent){if(silent===true){this.unselect(true);} +Ext.tree.TreeNode.superclass.destroy.call(this,silent);Ext.destroy(this.ui,this.loader);this.ui=this.loader=null;},onIdChange:function(id){this.ui.onIdChange(id);}});Ext.tree.TreePanel.nodeTypes.node=Ext.tree.TreeNode;Ext.tree.AsyncTreeNode=function(config){this.loaded=config&&config.loaded===true;this.loading=false;Ext.tree.AsyncTreeNode.superclass.constructor.apply(this,arguments);this.addEvents('beforeload','load');};Ext.extend(Ext.tree.AsyncTreeNode,Ext.tree.TreeNode,{expand:function(deep,anim,callback,scope){if(this.loading){var timer;var f=function(){if(!this.loading){clearInterval(timer);this.expand(deep,anim,callback,scope);}}.createDelegate(this);timer=setInterval(f,200);return;} +if(!this.loaded){if(this.fireEvent("beforeload",this)===false){return;} +this.loading=true;this.ui.beforeLoad(this);var loader=this.loader||this.attributes.loader||this.getOwnerTree().getLoader();if(loader){loader.load(this,this.loadComplete.createDelegate(this,[deep,anim,callback,scope]),this);return;}} +Ext.tree.AsyncTreeNode.superclass.expand.call(this,deep,anim,callback,scope);},isLoading:function(){return this.loading;},loadComplete:function(deep,anim,callback,scope){this.loading=false;this.loaded=true;this.ui.afterLoad(this);this.fireEvent("load",this);this.expand(deep,anim,callback,scope);},isLoaded:function(){return this.loaded;},hasChildNodes:function(){if(!this.isLeaf()&&!this.loaded){return true;}else{return Ext.tree.AsyncTreeNode.superclass.hasChildNodes.call(this);}},reload:function(callback,scope){this.collapse(false,false);while(this.firstChild){this.removeChild(this.firstChild).destroy();} +this.childrenRendered=false;this.loaded=false;if(this.isHiddenRoot()){this.expanded=false;} +this.expand(false,false,callback,scope);}});Ext.tree.TreePanel.nodeTypes.async=Ext.tree.AsyncTreeNode;Ext.tree.TreeNodeUI=Ext.extend(Object,{constructor:function(node){Ext.apply(this,{node:node,rendered:false,animating:false,wasLeaf:true,ecc:'x-tree-ec-icon x-tree-elbow',emptyIcon:Ext.BLANK_IMAGE_URL});},removeChild:function(node){if(this.rendered){this.ctNode.removeChild(node.ui.getEl());}},beforeLoad:function(){this.addClass("x-tree-node-loading");},afterLoad:function(){this.removeClass("x-tree-node-loading");},onTextChange:function(node,text,oldText){if(this.rendered){this.textNode.innerHTML=text;}},onIconClsChange:function(node,cls,oldCls){if(this.rendered){Ext.fly(this.iconNode).replaceClass(oldCls,cls);}},onIconChange:function(node,icon){if(this.rendered){var empty=Ext.isEmpty(icon);this.iconNode.src=empty?this.emptyIcon:icon;Ext.fly(this.iconNode)[empty?'removeClass':'addClass']('x-tree-node-inline-icon');}},onTipChange:function(node,tip,title){if(this.rendered){var hasTitle=Ext.isDefined(title);if(this.textNode.setAttributeNS){this.textNode.setAttributeNS("ext","qtip",tip);if(hasTitle){this.textNode.setAttributeNS("ext","qtitle",title);}}else{this.textNode.setAttribute("ext:qtip",tip);if(hasTitle){this.textNode.setAttribute("ext:qtitle",title);}}}},onHrefChange:function(node,href,target){if(this.rendered){this.anchor.href=this.getHref(href);if(Ext.isDefined(target)){this.anchor.target=target;}}},onClsChange:function(node,cls,oldCls){if(this.rendered){Ext.fly(this.elNode).replaceClass(oldCls,cls);}},onDisableChange:function(node,state){this.disabled=state;if(this.checkbox){this.checkbox.disabled=state;} +this[state?'addClass':'removeClass']('x-tree-node-disabled');},onSelectedChange:function(state){if(state){this.focus();this.addClass("x-tree-selected");}else{this.removeClass("x-tree-selected");}},onMove:function(tree,node,oldParent,newParent,index,refNode){this.childIndent=null;if(this.rendered){var targetNode=newParent.ui.getContainer();if(!targetNode){this.holder=document.createElement("div");this.holder.appendChild(this.wrap);return;} +var insertBefore=refNode?refNode.ui.getEl():null;if(insertBefore){targetNode.insertBefore(this.wrap,insertBefore);}else{targetNode.appendChild(this.wrap);} +this.node.renderIndent(true,oldParent!=newParent);}},addClass:function(cls){if(this.elNode){Ext.fly(this.elNode).addClass(cls);}},removeClass:function(cls){if(this.elNode){Ext.fly(this.elNode).removeClass(cls);}},remove:function(){if(this.rendered){this.holder=document.createElement("div");this.holder.appendChild(this.wrap);}},fireEvent:function(){return this.node.fireEvent.apply(this.node,arguments);},initEvents:function(){this.node.on("move",this.onMove,this);if(this.node.disabled){this.onDisableChange(this.node,true);} +if(this.node.hidden){this.hide();} +var ot=this.node.getOwnerTree();var dd=ot.enableDD||ot.enableDrag||ot.enableDrop;if(dd&&(!this.node.isRoot||ot.rootVisible)){Ext.dd.Registry.register(this.elNode,{node:this.node,handles:this.getDDHandles(),isHandle:false});}},getDDHandles:function(){return[this.iconNode,this.textNode,this.elNode];},hide:function(){this.node.hidden=true;if(this.wrap){this.wrap.style.display="none";}},show:function(){this.node.hidden=false;if(this.wrap){this.wrap.style.display="";}},onContextMenu:function(e){if(this.node.hasListener("contextmenu")||this.node.getOwnerTree().hasListener("contextmenu")){e.preventDefault();this.focus();this.fireEvent("contextmenu",this.node,e);}},onClick:function(e){if(this.dropping){e.stopEvent();return;} +if(this.fireEvent("beforeclick",this.node,e)!==false){var a=e.getTarget('a');if(!this.disabled&&this.node.attributes.href&&a){this.fireEvent("click",this.node,e);return;}else if(a&&e.ctrlKey){e.stopEvent();} +e.preventDefault();if(this.disabled){return;} +if(this.node.attributes.singleClickExpand&&!this.animating&&this.node.isExpandable()){this.node.toggle();} +this.fireEvent("click",this.node,e);}else{e.stopEvent();}},onDblClick:function(e){e.preventDefault();if(this.disabled){return;} +if(this.fireEvent("beforedblclick",this.node,e)!==false){if(this.checkbox){this.toggleCheck();} +if(!this.animating&&this.node.isExpandable()){this.node.toggle();} +this.fireEvent("dblclick",this.node,e);}},onOver:function(e){this.addClass('x-tree-node-over');},onOut:function(e){this.removeClass('x-tree-node-over');},onCheckChange:function(){var checked=this.checkbox.checked;this.checkbox.defaultChecked=checked;this.node.attributes.checked=checked;this.fireEvent('checkchange',this.node,checked);},ecClick:function(e){if(!this.animating&&this.node.isExpandable()){this.node.toggle();}},startDrop:function(){this.dropping=true;},endDrop:function(){setTimeout(function(){this.dropping=false;}.createDelegate(this),50);},expand:function(){this.updateExpandIcon();this.ctNode.style.display="";},focus:function(){if(!this.node.preventHScroll){try{this.anchor.focus();}catch(e){}}else{try{var noscroll=this.node.getOwnerTree().getTreeEl().dom;var l=noscroll.scrollLeft;this.anchor.focus();noscroll.scrollLeft=l;}catch(e){}}},toggleCheck:function(value){var cb=this.checkbox;if(cb){cb.checked=(value===undefined?!cb.checked:value);this.onCheckChange();}},blur:function(){try{this.anchor.blur();}catch(e){}},animExpand:function(callback){var ct=Ext.get(this.ctNode);ct.stopFx();if(!this.node.isExpandable()){this.updateExpandIcon();this.ctNode.style.display="";Ext.callback(callback);return;} +this.animating=true;this.updateExpandIcon();ct.slideIn('t',{callback:function(){this.animating=false;Ext.callback(callback);},scope:this,duration:this.node.ownerTree.duration||.25});},highlight:function(){var tree=this.node.getOwnerTree();Ext.fly(this.wrap).highlight(tree.hlColor||"C3DAF9",{endColor:tree.hlBaseColor});},collapse:function(){this.updateExpandIcon();this.ctNode.style.display="none";},animCollapse:function(callback){var ct=Ext.get(this.ctNode);ct.enableDisplayMode('block');ct.stopFx();this.animating=true;this.updateExpandIcon();ct.slideOut('t',{callback:function(){this.animating=false;Ext.callback(callback);},scope:this,duration:this.node.ownerTree.duration||.25});},getContainer:function(){return this.ctNode;},getEl:function(){return this.wrap;},appendDDGhost:function(ghostNode){ghostNode.appendChild(this.elNode.cloneNode(true));},getDDRepairXY:function(){return Ext.lib.Dom.getXY(this.iconNode);},onRender:function(){this.render();},render:function(bulkRender){var n=this.node,a=n.attributes;var targetNode=n.parentNode?n.parentNode.ui.getContainer():n.ownerTree.innerCt.dom;if(!this.rendered){this.rendered=true;this.renderElements(n,a,targetNode,bulkRender);if(a.qtip){this.onTipChange(n,a.qtip,a.qtipTitle);}else if(a.qtipCfg){a.qtipCfg.target=Ext.id(this.textNode);Ext.QuickTips.register(a.qtipCfg);} +this.initEvents();if(!this.node.expanded){this.updateExpandIcon(true);}}else{if(bulkRender===true){targetNode.appendChild(this.wrap);}}},renderElements:function(n,a,targetNode,bulkRender){this.indentMarkup=n.parentNode?n.parentNode.ui.getChildIndent():'';var cb=Ext.isBoolean(a.checked),nel,href=this.getHref(a.href),buf=['<li class="x-tree-node"><div ext:tree-node-id="',n.id,'" class="x-tree-node-el x-tree-node-leaf x-unselectable ',a.cls,'" unselectable="on">','<span class="x-tree-node-indent">',this.indentMarkup,"</span>",'<img alt="" src="',this.emptyIcon,'" class="x-tree-ec-icon x-tree-elbow" />','<img alt="" src="',a.icon||this.emptyIcon,'" class="x-tree-node-icon',(a.icon?" x-tree-node-inline-icon":""),(a.iconCls?" "+a.iconCls:""),'" unselectable="on" />',cb?('<input class="x-tree-node-cb" type="checkbox" '+(a.checked?'checked="checked" />':'/>')):'','<a hidefocus="on" class="x-tree-node-anchor" href="',href,'" tabIndex="1" ',a.hrefTarget?' target="'+a.hrefTarget+'"':"",'><span unselectable="on">',n.text,"</span></a></div>",'<ul class="x-tree-node-ct" style="display:none;"></ul>',"</li>"].join('');if(bulkRender!==true&&n.nextSibling&&(nel=n.nextSibling.ui.getEl())){this.wrap=Ext.DomHelper.insertHtml("beforeBegin",nel,buf);}else{this.wrap=Ext.DomHelper.insertHtml("beforeEnd",targetNode,buf);} +this.elNode=this.wrap.childNodes[0];this.ctNode=this.wrap.childNodes[1];var cs=this.elNode.childNodes;this.indentNode=cs[0];this.ecNode=cs[1];this.iconNode=cs[2];var index=3;if(cb){this.checkbox=cs[3];this.checkbox.defaultChecked=this.checkbox.checked;index++;} +this.anchor=cs[index];this.textNode=cs[index].firstChild;},getHref:function(href){return Ext.isEmpty(href)?(Ext.isGecko?'':'#'):href;},getAnchor:function(){return this.anchor;},getTextEl:function(){return this.textNode;},getIconEl:function(){return this.iconNode;},isChecked:function(){return this.checkbox?this.checkbox.checked:false;},updateExpandIcon:function(){if(this.rendered){var n=this.node,c1,c2,cls=n.isLast()?"x-tree-elbow-end":"x-tree-elbow",hasChild=n.hasChildNodes();if(hasChild||n.attributes.expandable){if(n.expanded){cls+="-minus";c1="x-tree-node-collapsed";c2="x-tree-node-expanded";}else{cls+="-plus";c1="x-tree-node-expanded";c2="x-tree-node-collapsed";} +if(this.wasLeaf){this.removeClass("x-tree-node-leaf");this.wasLeaf=false;} +if(this.c1!=c1||this.c2!=c2){Ext.fly(this.elNode).replaceClass(c1,c2);this.c1=c1;this.c2=c2;}}else{if(!this.wasLeaf){Ext.fly(this.elNode).replaceClass("x-tree-node-expanded","x-tree-node-collapsed");delete this.c1;delete this.c2;this.wasLeaf=true;}} +var ecc="x-tree-ec-icon "+cls;if(this.ecc!=ecc){this.ecNode.className=ecc;this.ecc=ecc;}}},onIdChange:function(id){if(this.rendered){this.elNode.setAttribute('ext:tree-node-id',id);}},getChildIndent:function(){if(!this.childIndent){var buf=[],p=this.node;while(p){if(!p.isRoot||(p.isRoot&&p.ownerTree.rootVisible)){if(!p.isLast()){buf.unshift('<img alt="" src="'+this.emptyIcon+'" class="x-tree-elbow-line" />');}else{buf.unshift('<img alt="" src="'+this.emptyIcon+'" class="x-tree-icon" />');}} +p=p.parentNode;} +this.childIndent=buf.join("");} +return this.childIndent;},renderIndent:function(){if(this.rendered){var indent="",p=this.node.parentNode;if(p){indent=p.ui.getChildIndent();} +if(this.indentMarkup!=indent){this.indentNode.innerHTML=indent;this.indentMarkup=indent;} +this.updateExpandIcon();}},destroy:function(){if(this.elNode){Ext.dd.Registry.unregister(this.elNode.id);} +Ext.each(['textnode','anchor','checkbox','indentNode','ecNode','iconNode','elNode','ctNode','wrap','holder'],function(el){if(this[el]){Ext.fly(this[el]).remove();delete this[el];}},this);delete this.node;}});Ext.tree.RootTreeNodeUI=Ext.extend(Ext.tree.TreeNodeUI,{render:function(){if(!this.rendered){var targetNode=this.node.ownerTree.innerCt.dom;this.node.expanded=true;targetNode.innerHTML='<div class="x-tree-root-node"></div>';this.wrap=this.ctNode=targetNode.firstChild;}},collapse:Ext.emptyFn,expand:Ext.emptyFn});Ext.tree.TreeLoader=function(config){this.baseParams={};Ext.apply(this,config);this.addEvents("beforeload","load","loadexception");Ext.tree.TreeLoader.superclass.constructor.call(this);if(Ext.isString(this.paramOrder)){this.paramOrder=this.paramOrder.split(/[\s,|]/);}};Ext.extend(Ext.tree.TreeLoader,Ext.util.Observable,{uiProviders:{},clearOnLoad:true,paramOrder:undefined,paramsAsHash:false,nodeParameter:'node',directFn:undefined,load:function(node,callback,scope){if(this.clearOnLoad){while(node.firstChild){node.removeChild(node.firstChild);}} +if(this.doPreload(node)){this.runCallback(callback,scope||node,[node]);}else if(this.directFn||this.dataUrl||this.url){this.requestData(node,callback,scope||node);}},doPreload:function(node){if(node.attributes.children){if(node.childNodes.length<1){var cs=node.attributes.children;node.beginUpdate();for(var i=0,len=cs.length;i<len;i++){var cn=node.appendChild(this.createNode(cs[i]));if(this.preloadChildren){this.doPreload(cn);}} +node.endUpdate();} +return true;} +return false;},getParams:function(node){var bp=Ext.apply({},this.baseParams),np=this.nodeParameter,po=this.paramOrder;np&&(bp[np]=node.id);if(this.directFn){var buf=[node.id];if(po){if(np&&po.indexOf(np)>-1){buf=[];} +for(var i=0,len=po.length;i<len;i++){buf.push(bp[po[i]]);}}else if(this.paramsAsHash){buf=[bp];} +return buf;}else{return bp;}},requestData:function(node,callback,scope){if(this.fireEvent("beforeload",this,node,callback)!==false){if(this.directFn){var args=this.getParams(node);args.push(this.processDirectResponse.createDelegate(this,[{callback:callback,node:node,scope:scope}],true));this.directFn.apply(window,args);}else{this.transId=Ext.Ajax.request({method:this.requestMethod,url:this.dataUrl||this.url,success:this.handleResponse,failure:this.handleFailure,scope:this,argument:{callback:callback,node:node,scope:scope},params:this.getParams(node)});}}else{this.runCallback(callback,scope||node,[]);}},processDirectResponse:function(result,response,args){if(response.status){this.handleResponse({responseData:Ext.isArray(result)?result:null,responseText:result,argument:args});}else{this.handleFailure({argument:args});}},runCallback:function(cb,scope,args){if(Ext.isFunction(cb)){cb.apply(scope,args);}},isLoading:function(){return!!this.transId;},abort:function(){if(this.isLoading()){Ext.Ajax.abort(this.transId);}},createNode:function(attr){if(this.baseAttrs){Ext.applyIf(attr,this.baseAttrs);} +if(this.applyLoader!==false&&!attr.loader){attr.loader=this;} +if(Ext.isString(attr.uiProvider)){attr.uiProvider=this.uiProviders[attr.uiProvider]||eval(attr.uiProvider);} +if(attr.nodeType){return new Ext.tree.TreePanel.nodeTypes[attr.nodeType](attr);}else{return attr.leaf?new Ext.tree.TreeNode(attr):new Ext.tree.AsyncTreeNode(attr);}},processResponse:function(response,node,callback,scope){var json=response.responseText;try{var o=response.responseData||Ext.decode(json);node.beginUpdate();for(var i=0,len=o.length;i<len;i++){var n=this.createNode(o[i]);if(n){node.appendChild(n);}} +node.endUpdate();this.runCallback(callback,scope||node,[node]);}catch(e){this.handleFailure(response);}},handleResponse:function(response){this.transId=false;var a=response.argument;this.processResponse(response,a.node,a.callback,a.scope);this.fireEvent("load",this,a.node,response);},handleFailure:function(response){this.transId=false;var a=response.argument;this.fireEvent("loadexception",this,a.node,response);this.runCallback(a.callback,a.scope||a.node,[a.node]);},destroy:function(){this.abort();this.purgeListeners();}});Ext.tree.TreeFilter=function(tree,config){this.tree=tree;this.filtered={};Ext.apply(this,config);};Ext.tree.TreeFilter.prototype={clearBlank:false,reverse:false,autoClear:false,remove:false,filter:function(value,attr,startNode){attr=attr||"text";var f;if(typeof value=="string"){var vlen=value.length;if(vlen==0&&this.clearBlank){this.clear();return;} +value=value.toLowerCase();f=function(n){return n.attributes[attr].substr(0,vlen).toLowerCase()==value;};}else if(value.exec){f=function(n){return value.test(n.attributes[attr]);};}else{throw'Illegal filter type, must be string or regex';} +this.filterBy(f,null,startNode);},filterBy:function(fn,scope,startNode){startNode=startNode||this.tree.root;if(this.autoClear){this.clear();} +var af=this.filtered,rv=this.reverse;var f=function(n){if(n==startNode){return true;} +if(af[n.id]){return false;} +var m=fn.call(scope||n,n);if(!m||rv){af[n.id]=n;n.ui.hide();return false;} +return true;};startNode.cascade(f);if(this.remove){for(var id in af){if(typeof id!="function"){var n=af[id];if(n&&n.parentNode){n.parentNode.removeChild(n);}}}}},clear:function(){var t=this.tree;var af=this.filtered;for(var id in af){if(typeof id!="function"){var n=af[id];if(n){n.ui.show();}}} +this.filtered={};}};Ext.tree.TreeSorter=Ext.extend(Object,{constructor:function(tree,config){Ext.apply(this,config);tree.on({scope:this,beforechildrenrendered:this.doSort,append:this.updateSort,insert:this.updateSort,textchange:this.updateSortParent});var desc=this.dir&&this.dir.toLowerCase()=='desc',prop=this.property||'text';sortType=this.sortType;folderSort=this.folderSort;caseSensitive=this.caseSensitive===true;leafAttr=this.leafAttr||'leaf';if(Ext.isString(sortType)){sortType=Ext.data.SortTypes[sortType];} +this.sortFn=function(n1,n2){var attr1=n1.attributes,attr2=n2.attributes;if(folderSort){if(attr1[leafAttr]&&!attr2[leafAttr]){return 1;} +if(!attr1[leafAttr]&&attr2[leafAttr]){return-1;}} +var prop1=attr1[prop],prop2=attr2[prop],v1=sortType?sortType(prop1):(caseSensitive?prop1:prop1.toUpperCase());v2=sortType?sortType(prop2):(caseSensitive?prop2:prop2.toUpperCase());if(v1<v2){return desc?1:-1;}else if(v1>v2){return desc?-1:1;} +return 0;};},doSort:function(node){node.sort(this.sortFn);},updateSort:function(tree,node){if(node.childrenRendered){this.doSort.defer(1,this,[node]);}},updateSortParent:function(node){var p=node.parentNode;if(p&&p.childrenRendered){this.doSort.defer(1,this,[p]);}}});if(Ext.dd.DropZone){Ext.tree.TreeDropZone=function(tree,config){this.allowParentInsert=config.allowParentInsert||false;this.allowContainerDrop=config.allowContainerDrop||false;this.appendOnly=config.appendOnly||false;Ext.tree.TreeDropZone.superclass.constructor.call(this,tree.getTreeEl(),config);this.tree=tree;this.dragOverData={};this.lastInsertClass="x-tree-no-status";};Ext.extend(Ext.tree.TreeDropZone,Ext.dd.DropZone,{ddGroup:"TreeDD",expandDelay:1000,expandNode:function(node){if(node.hasChildNodes()&&!node.isExpanded()){node.expand(false,null,this.triggerCacheRefresh.createDelegate(this));}},queueExpand:function(node){this.expandProcId=this.expandNode.defer(this.expandDelay,this,[node]);},cancelExpand:function(){if(this.expandProcId){clearTimeout(this.expandProcId);this.expandProcId=false;}},isValidDropPoint:function(n,pt,dd,e,data){if(!n||!data){return false;} +var targetNode=n.node;var dropNode=data.node;if(!(targetNode&&targetNode.isTarget&&pt)){return false;} +if(pt=="append"&&targetNode.allowChildren===false){return false;} +if((pt=="above"||pt=="below")&&(targetNode.parentNode&&targetNode.parentNode.allowChildren===false)){return false;} +if(dropNode&&(targetNode==dropNode||dropNode.contains(targetNode))){return false;} +var overEvent=this.dragOverData;overEvent.tree=this.tree;overEvent.target=targetNode;overEvent.data=data;overEvent.point=pt;overEvent.source=dd;overEvent.rawEvent=e;overEvent.dropNode=dropNode;overEvent.cancel=false;var result=this.tree.fireEvent("nodedragover",overEvent);return overEvent.cancel===false&&result!==false;},getDropPoint:function(e,n,dd){var tn=n.node;if(tn.isRoot){return tn.allowChildren!==false?"append":false;} +var dragEl=n.ddel;var t=Ext.lib.Dom.getY(dragEl),b=t+dragEl.offsetHeight;var y=Ext.lib.Event.getPageY(e);var noAppend=tn.allowChildren===false||tn.isLeaf();if(this.appendOnly||tn.parentNode.allowChildren===false){return noAppend?false:"append";} +var noBelow=false;if(!this.allowParentInsert){noBelow=tn.hasChildNodes()&&tn.isExpanded();} +var q=(b-t)/(noAppend?2:3);if(y>=t&&y<(t+q)){return"above";}else if(!noBelow&&(noAppend||y>=b-q&&y<=b)){return"below";}else{return"append";}},onNodeEnter:function(n,dd,e,data){this.cancelExpand();},onContainerOver:function(dd,e,data){if(this.allowContainerDrop&&this.isValidDropPoint({ddel:this.tree.getRootNode().ui.elNode,node:this.tree.getRootNode()},"append",dd,e,data)){return this.dropAllowed;} +return this.dropNotAllowed;},onNodeOver:function(n,dd,e,data){var pt=this.getDropPoint(e,n,dd);var node=n.node;if(!this.expandProcId&&pt=="append"&&node.hasChildNodes()&&!n.node.isExpanded()){this.queueExpand(node);}else if(pt!="append"){this.cancelExpand();} +var returnCls=this.dropNotAllowed;if(this.isValidDropPoint(n,pt,dd,e,data)){if(pt){var el=n.ddel;var cls;if(pt=="above"){returnCls=n.node.isFirst()?"x-tree-drop-ok-above":"x-tree-drop-ok-between";cls="x-tree-drag-insert-above";}else if(pt=="below"){returnCls=n.node.isLast()?"x-tree-drop-ok-below":"x-tree-drop-ok-between";cls="x-tree-drag-insert-below";}else{returnCls="x-tree-drop-ok-append";cls="x-tree-drag-append";} +if(this.lastInsertClass!=cls){Ext.fly(el).replaceClass(this.lastInsertClass,cls);this.lastInsertClass=cls;}}} +return returnCls;},onNodeOut:function(n,dd,e,data){this.cancelExpand();this.removeDropIndicators(n);},onNodeDrop:function(n,dd,e,data){var point=this.getDropPoint(e,n,dd);var targetNode=n.node;targetNode.ui.startDrop();if(!this.isValidDropPoint(n,point,dd,e,data)){targetNode.ui.endDrop();return false;} +var dropNode=data.node||(dd.getTreeNode?dd.getTreeNode(data,targetNode,point,e):null);return this.processDrop(targetNode,data,point,dd,e,dropNode);},onContainerDrop:function(dd,e,data){if(this.allowContainerDrop&&this.isValidDropPoint({ddel:this.tree.getRootNode().ui.elNode,node:this.tree.getRootNode()},"append",dd,e,data)){var targetNode=this.tree.getRootNode();targetNode.ui.startDrop();var dropNode=data.node||(dd.getTreeNode?dd.getTreeNode(data,targetNode,'append',e):null);return this.processDrop(targetNode,data,'append',dd,e,dropNode);} +return false;},processDrop:function(target,data,point,dd,e,dropNode){var dropEvent={tree:this.tree,target:target,data:data,point:point,source:dd,rawEvent:e,dropNode:dropNode,cancel:!dropNode,dropStatus:false};var retval=this.tree.fireEvent("beforenodedrop",dropEvent);if(retval===false||dropEvent.cancel===true||!dropEvent.dropNode){target.ui.endDrop();return dropEvent.dropStatus;} +target=dropEvent.target;if(point=='append'&&!target.isExpanded()){target.expand(false,null,function(){this.completeDrop(dropEvent);}.createDelegate(this));}else{this.completeDrop(dropEvent);} +return true;},completeDrop:function(de){var ns=de.dropNode,p=de.point,t=de.target;if(!Ext.isArray(ns)){ns=[ns];} +var n;for(var i=0,len=ns.length;i<len;i++){n=ns[i];if(p=="above"){t.parentNode.insertBefore(n,t);}else if(p=="below"){t.parentNode.insertBefore(n,t.nextSibling);}else{t.appendChild(n);}} +n.ui.focus();if(Ext.enableFx&&this.tree.hlDrop){n.ui.highlight();} +t.ui.endDrop();this.tree.fireEvent("nodedrop",de);},afterNodeMoved:function(dd,data,e,targetNode,dropNode){if(Ext.enableFx&&this.tree.hlDrop){dropNode.ui.focus();dropNode.ui.highlight();} +this.tree.fireEvent("nodedrop",this.tree,targetNode,data,dd,e);},getTree:function(){return this.tree;},removeDropIndicators:function(n){if(n&&n.ddel){var el=n.ddel;Ext.fly(el).removeClass(["x-tree-drag-insert-above","x-tree-drag-insert-below","x-tree-drag-append"]);this.lastInsertClass="_noclass";}},beforeDragDrop:function(target,e,id){this.cancelExpand();return true;},afterRepair:function(data){if(data&&Ext.enableFx){data.node.ui.highlight();} +this.hideProxy();}});} +if(Ext.dd.DragZone){Ext.tree.TreeDragZone=function(tree,config){Ext.tree.TreeDragZone.superclass.constructor.call(this,tree.innerCt,config);this.tree=tree;};Ext.extend(Ext.tree.TreeDragZone,Ext.dd.DragZone,{ddGroup:"TreeDD",onBeforeDrag:function(data,e){var n=data.node;return n&&n.draggable&&!n.disabled;},onInitDrag:function(e){var data=this.dragData;this.tree.getSelectionModel().select(data.node);this.tree.eventModel.disable();this.proxy.update("");data.node.ui.appendDDGhost(this.proxy.ghost.dom);this.tree.fireEvent("startdrag",this.tree,data.node,e);},getRepairXY:function(e,data){return data.node.ui.getDDRepairXY();},onEndDrag:function(data,e){this.tree.eventModel.enable.defer(100,this.tree.eventModel);this.tree.fireEvent("enddrag",this.tree,data.node,e);},onValidDrop:function(dd,e,id){this.tree.fireEvent("dragdrop",this.tree,this.dragData.node,dd,e);this.hideProxy();},beforeInvalidDrop:function(e,id){var sm=this.tree.getSelectionModel();sm.clearSelections();sm.select(this.dragData.node);},afterRepair:function(){if(Ext.enableFx&&this.tree.hlDrop){Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor||"c3daf9");} +this.dragging=false;}});} +Ext.tree.TreeEditor=function(tree,fc,config){fc=fc||{};var field=fc.events?fc:new Ext.form.TextField(fc);Ext.tree.TreeEditor.superclass.constructor.call(this,field,config);this.tree=tree;if(!tree.rendered){tree.on('render',this.initEditor,this);}else{this.initEditor(tree);}};Ext.extend(Ext.tree.TreeEditor,Ext.Editor,{alignment:"l-l",autoSize:false,hideEl:false,cls:"x-small-editor x-tree-editor",shim:false,shadow:"frame",maxWidth:250,editDelay:350,initEditor:function(tree){tree.on({scope:this,beforeclick:this.beforeNodeClick,dblclick:this.onNodeDblClick});this.on({scope:this,complete:this.updateNode,beforestartedit:this.fitToTree,specialkey:this.onSpecialKey});this.on('startedit',this.bindScroll,this,{delay:10});},fitToTree:function(ed,el){var td=this.tree.getTreeEl().dom,nd=el.dom;if(td.scrollLeft>nd.offsetLeft){td.scrollLeft=nd.offsetLeft;} +var w=Math.min(this.maxWidth,(td.clientWidth>20?td.clientWidth:td.offsetWidth)-Math.max(0,nd.offsetLeft-td.scrollLeft)-5);this.setSize(w,'');},triggerEdit:function(node,defer){this.completeEdit();if(node.attributes.editable!==false){this.editNode=node;if(this.tree.autoScroll){Ext.fly(node.ui.getEl()).scrollIntoView(this.tree.body);} +var value=node.text||'';if(!Ext.isGecko&&Ext.isEmpty(node.text)){node.setText(' ');} +this.autoEditTimer=this.startEdit.defer(this.editDelay,this,[node.ui.textNode,value]);return false;}},bindScroll:function(){this.tree.getTreeEl().on('scroll',this.cancelEdit,this);},beforeNodeClick:function(node,e){clearTimeout(this.autoEditTimer);if(this.tree.getSelectionModel().isSelected(node)){e.stopEvent();return this.triggerEdit(node);}},onNodeDblClick:function(node,e){clearTimeout(this.autoEditTimer);},updateNode:function(ed,value){this.tree.getTreeEl().un('scroll',this.cancelEdit,this);this.editNode.setText(value);},onHide:function(){Ext.tree.TreeEditor.superclass.onHide.call(this);if(this.editNode){this.editNode.ui.focus.defer(50,this.editNode.ui);}},onSpecialKey:function(field,e){var k=e.getKey();if(k==e.ESC){e.stopEvent();this.cancelEdit();}else if(k==e.ENTER&&!e.hasModifier()){e.stopEvent();this.completeEdit();}},onDestroy:function(){clearTimeout(this.autoEditTimer);Ext.tree.TreeEditor.superclass.onDestroy.call(this);var tree=this.tree;tree.un('beforeclick',this.beforeNodeClick,this);tree.un('dblclick',this.onNodeDblClick,this);}});Ext.Window=Ext.extend(Ext.Panel,{baseCls:'x-window',resizable:true,draggable:true,closable:true,closeAction:'close',constrain:false,constrainHeader:false,plain:false,minimizable:false,maximizable:false,minHeight:100,minWidth:200,expandOnShow:true,showAnimDuration:0.25,hideAnimDuration:0.25,collapsible:false,initHidden:undefined,hidden:true,elements:'header,body',frame:true,floating:true,initComponent:function(){this.initTools();Ext.Window.superclass.initComponent.call(this);this.addEvents('resize','maximize','minimize','restore');if(Ext.isDefined(this.initHidden)){this.hidden=this.initHidden;} +if(this.hidden===false){this.hidden=true;this.show();}},getState:function(){return Ext.apply(Ext.Window.superclass.getState.call(this)||{},this.getBox(true));},onRender:function(ct,position){Ext.Window.superclass.onRender.call(this,ct,position);if(this.plain){this.el.addClass('x-window-plain');} +this.focusEl=this.el.createChild({tag:'a',href:'#',cls:'x-dlg-focus',tabIndex:'-1',html:' '});this.focusEl.swallowEvent('click',true);this.proxy=this.el.createProxy('x-window-proxy');this.proxy.enableDisplayMode('block');if(this.modal){this.mask=this.container.createChild({cls:'ext-el-mask'},this.el.dom);this.mask.enableDisplayMode('block');this.mask.hide();this.mon(this.mask,'click',this.focus,this);} +if(this.maximizable){this.mon(this.header,'dblclick',this.toggleMaximize,this);}},initEvents:function(){Ext.Window.superclass.initEvents.call(this);if(this.animateTarget){this.setAnimateTarget(this.animateTarget);} +if(this.resizable){this.resizer=new Ext.Resizable(this.el,{minWidth:this.minWidth,minHeight:this.minHeight,handles:this.resizeHandles||'all',pinned:true,resizeElement:this.resizerAction,handleCls:'x-window-handle'});this.resizer.window=this;this.mon(this.resizer,'beforeresize',this.beforeResize,this);} +if(this.draggable){this.header.addClass('x-window-draggable');} +this.mon(this.el,'mousedown',this.toFront,this);this.manager=this.manager||Ext.WindowMgr;this.manager.register(this);if(this.maximized){this.maximized=false;this.maximize();} +if(this.closable){var km=this.getKeyMap();km.on(27,this.onEsc,this);km.disable();}},initDraggable:function(){this.dd=new Ext.Window.DD(this);},onEsc:function(k,e){e.stopEvent();this[this.closeAction]();},beforeDestroy:function(){if(this.rendered){this.hide();this.clearAnchor();Ext.destroy(this.focusEl,this.resizer,this.dd,this.proxy,this.mask);} +Ext.Window.superclass.beforeDestroy.call(this);},onDestroy:function(){if(this.manager){this.manager.unregister(this);} +Ext.Window.superclass.onDestroy.call(this);},initTools:function(){if(this.minimizable){this.addTool({id:'minimize',handler:this.minimize.createDelegate(this,[])});} +if(this.maximizable){this.addTool({id:'maximize',handler:this.maximize.createDelegate(this,[])});this.addTool({id:'restore',handler:this.restore.createDelegate(this,[]),hidden:true});} +if(this.closable){this.addTool({id:'close',handler:this[this.closeAction].createDelegate(this,[])});}},resizerAction:function(){var box=this.proxy.getBox();this.proxy.hide();this.window.handleResize(box);return box;},beforeResize:function(){this.resizer.minHeight=Math.max(this.minHeight,this.getFrameHeight()+40);this.resizer.minWidth=Math.max(this.minWidth,this.getFrameWidth()+40);this.resizeBox=this.el.getBox();},updateHandles:function(){if(Ext.isIE&&this.resizer){this.resizer.syncHandleHeight();this.el.repaint();}},handleResize:function(box){var rz=this.resizeBox;if(rz.x!=box.x||rz.y!=box.y){this.updateBox(box);}else{this.setSize(box);if(Ext.isIE6&&Ext.isStrict){this.doLayout();}} +this.focus();this.updateHandles();this.saveState();},focus:function(){var f=this.focusEl,db=this.defaultButton,t=typeof db,el,ct;if(Ext.isDefined(db)){if(Ext.isNumber(db)&&this.fbar){f=this.fbar.items.get(db);}else if(Ext.isString(db)){f=Ext.getCmp(db);}else{f=db;} +el=f.getEl();ct=Ext.getDom(this.container);if(el&&ct){if(ct!=document.body&&!Ext.lib.Region.getRegion(ct).contains(Ext.lib.Region.getRegion(el.dom))){return;}}} +f=f||this.focusEl;f.focus.defer(10,f);},setAnimateTarget:function(el){el=Ext.get(el);this.animateTarget=el;},beforeShow:function(){delete this.el.lastXY;delete this.el.lastLT;if(this.x===undefined||this.y===undefined){var xy=this.el.getAlignToXY(this.container,'c-c');var pos=this.el.translatePoints(xy[0],xy[1]);this.x=this.x===undefined?pos.left:this.x;this.y=this.y===undefined?pos.top:this.y;} +this.el.setLeftTop(this.x,this.y);if(this.expandOnShow){this.expand(false);} +if(this.modal){Ext.getBody().addClass('x-body-masked');this.mask.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true));this.mask.show();}},show:function(animateTarget,cb,scope){if(!this.rendered){this.render(Ext.getBody());} +if(this.hidden===false){this.toFront();return this;} +if(this.fireEvent('beforeshow',this)===false){return this;} +if(cb){this.on('show',cb,scope,{single:true});} +this.hidden=false;if(Ext.isDefined(animateTarget)){this.setAnimateTarget(animateTarget);} +this.beforeShow();if(this.animateTarget){this.animShow();}else{this.afterShow();} +return this;},afterShow:function(isAnim){if(this.isDestroyed){return false;} +this.proxy.hide();this.el.setStyle('display','block');this.el.show();if(this.maximized){this.fitContainer();} +if(Ext.isMac&&Ext.isGecko2){this.cascade(this.setAutoScroll);} +if(this.monitorResize||this.modal||this.constrain||this.constrainHeader){Ext.EventManager.onWindowResize(this.onWindowResize,this);} +this.doConstrain();this.doLayout();if(this.keyMap){this.keyMap.enable();} +this.toFront();this.updateHandles();if(isAnim&&(Ext.isIE||Ext.isWebKit)){var sz=this.getSize();this.onResize(sz.width,sz.height);} +this.onShow();this.fireEvent('show',this);},animShow:function(){this.proxy.show();this.proxy.setBox(this.animateTarget.getBox());this.proxy.setOpacity(0);var b=this.getBox();this.el.setStyle('display','none');this.proxy.shift(Ext.apply(b,{callback:this.afterShow.createDelegate(this,[true],false),scope:this,easing:'easeNone',duration:this.showAnimDuration,opacity:0.5}));},hide:function(animateTarget,cb,scope){if(this.hidden||this.fireEvent('beforehide',this)===false){return this;} +if(cb){this.on('hide',cb,scope,{single:true});} +this.hidden=true;if(animateTarget!==undefined){this.setAnimateTarget(animateTarget);} +if(this.modal){this.mask.hide();Ext.getBody().removeClass('x-body-masked');} +if(this.animateTarget){this.animHide();}else{this.el.hide();this.afterHide();} +return this;},afterHide:function(){this.proxy.hide();if(this.monitorResize||this.modal||this.constrain||this.constrainHeader){Ext.EventManager.removeResizeListener(this.onWindowResize,this);} +if(this.keyMap){this.keyMap.disable();} +this.onHide();this.fireEvent('hide',this);},animHide:function(){this.proxy.setOpacity(0.5);this.proxy.show();var tb=this.getBox(false);this.proxy.setBox(tb);this.el.hide();this.proxy.shift(Ext.apply(this.animateTarget.getBox(),{callback:this.afterHide,scope:this,duration:this.hideAnimDuration,easing:'easeNone',opacity:0}));},onShow:Ext.emptyFn,onHide:Ext.emptyFn,onWindowResize:function(){if(this.maximized){this.fitContainer();} +if(this.modal){this.mask.setSize('100%','100%');var force=this.mask.dom.offsetHeight;this.mask.setSize(Ext.lib.Dom.getViewWidth(true),Ext.lib.Dom.getViewHeight(true));} +this.doConstrain();},doConstrain:function(){if(this.constrain||this.constrainHeader){var offsets;if(this.constrain){offsets={right:this.el.shadowOffset,left:this.el.shadowOffset,bottom:this.el.shadowOffset};}else{var s=this.getSize();offsets={right:-(s.width-100),bottom:-(s.height-25)};} +var xy=this.el.getConstrainToXY(this.container,true,offsets);if(xy){this.setPosition(xy[0],xy[1]);}}},ghost:function(cls){var ghost=this.createGhost(cls);var box=this.getBox(true);ghost.setLeftTop(box.x,box.y);ghost.setWidth(box.width);this.el.hide();this.activeGhost=ghost;return ghost;},unghost:function(show,matchPosition){if(!this.activeGhost){return;} +if(show!==false){this.el.show();this.focus.defer(10,this);if(Ext.isMac&&Ext.isGecko2){this.cascade(this.setAutoScroll);}} +if(matchPosition!==false){this.setPosition(this.activeGhost.getLeft(true),this.activeGhost.getTop(true));} +this.activeGhost.hide();this.activeGhost.remove();delete this.activeGhost;},minimize:function(){this.fireEvent('minimize',this);return this;},close:function(){if(this.fireEvent('beforeclose',this)!==false){if(this.hidden){this.doClose();}else{this.hide(null,this.doClose,this);}}},doClose:function(){this.fireEvent('close',this);this.destroy();},maximize:function(){if(!this.maximized){this.expand(false);this.restoreSize=this.getSize();this.restorePos=this.getPosition(true);if(this.maximizable){this.tools.maximize.hide();this.tools.restore.show();} +this.maximized=true;this.el.disableShadow();if(this.dd){this.dd.lock();} +if(this.collapsible){this.tools.toggle.hide();} +this.el.addClass('x-window-maximized');this.container.addClass('x-window-maximized-ct');this.setPosition(0,0);this.fitContainer();this.fireEvent('maximize',this);} +return this;},restore:function(){if(this.maximized){var t=this.tools;this.el.removeClass('x-window-maximized');if(t.restore){t.restore.hide();} +if(t.maximize){t.maximize.show();} +this.setPosition(this.restorePos[0],this.restorePos[1]);this.setSize(this.restoreSize.width,this.restoreSize.height);delete this.restorePos;delete this.restoreSize;this.maximized=false;this.el.enableShadow(true);if(this.dd){this.dd.unlock();} +if(this.collapsible&&t.toggle){t.toggle.show();} +this.container.removeClass('x-window-maximized-ct');this.doConstrain();this.fireEvent('restore',this);} +return this;},toggleMaximize:function(){return this[this.maximized?'restore':'maximize']();},fitContainer:function(){var vs=this.container.getViewSize(false);this.setSize(vs.width,vs.height);},setZIndex:function(index){if(this.modal){this.mask.setStyle('z-index',index);} +this.el.setZIndex(++index);index+=5;if(this.resizer){this.resizer.proxy.setStyle('z-index',++index);} +this.lastZIndex=index;},alignTo:function(element,position,offsets){var xy=this.el.getAlignToXY(element,position,offsets);this.setPagePosition(xy[0],xy[1]);return this;},anchorTo:function(el,alignment,offsets,monitorScroll){this.clearAnchor();this.anchorTarget={el:el,alignment:alignment,offsets:offsets};Ext.EventManager.onWindowResize(this.doAnchor,this);var tm=typeof monitorScroll;if(tm!='undefined'){Ext.EventManager.on(window,'scroll',this.doAnchor,this,{buffer:tm=='number'?monitorScroll:50});} +return this.doAnchor();},doAnchor:function(){var o=this.anchorTarget;this.alignTo(o.el,o.alignment,o.offsets);return this;},clearAnchor:function(){if(this.anchorTarget){Ext.EventManager.removeResizeListener(this.doAnchor,this);Ext.EventManager.un(window,'scroll',this.doAnchor,this);delete this.anchorTarget;} +return this;},toFront:function(e){if(this.manager.bringToFront(this)){if(!e||!e.getTarget().focus){this.focus();}} +return this;},setActive:function(active){if(active){if(!this.maximized){this.el.enableShadow(true);} +this.fireEvent('activate',this);}else{this.el.disableShadow();this.fireEvent('deactivate',this);}},toBack:function(){this.manager.sendToBack(this);return this;},center:function(){var xy=this.el.getAlignToXY(this.container,'c-c');this.setPagePosition(xy[0],xy[1]);return this;}});Ext.reg('window',Ext.Window);Ext.Window.DD=Ext.extend(Ext.dd.DD,{constructor:function(win){this.win=win;Ext.Window.DD.superclass.constructor.call(this,win.el.id,'WindowDD-'+win.id);this.setHandleElId(win.header.id);this.scroll=false;},moveOnly:true,headerOffsets:[100,25],startDrag:function(){var w=this.win;this.proxy=w.ghost(w.initialConfig.cls);if(w.constrain!==false){var so=w.el.shadowOffset;this.constrainTo(w.container,{right:so,left:so,bottom:so});}else if(w.constrainHeader!==false){var s=this.proxy.getSize();this.constrainTo(w.container,{right:-(s.width-this.headerOffsets[0]),bottom:-(s.height-this.headerOffsets[1])});}},b4Drag:Ext.emptyFn,onDrag:function(e){this.alignElWithMouse(this.proxy,e.getPageX(),e.getPageY());},endDrag:function(e){this.win.unghost();this.win.saveState();}});Ext.WindowGroup=function(){var list={};var accessList=[];var front=null;var sortWindows=function(d1,d2){return(!d1._lastAccess||d1._lastAccess<d2._lastAccess)?-1:1;};var orderWindows=function(){var a=accessList,len=a.length;if(len>0){a.sort(sortWindows);var seed=a[0].manager.zseed;for(var i=0;i<len;i++){var win=a[i];if(win&&!win.hidden){win.setZIndex(seed+(i*10));}}} +activateLast();};var setActiveWin=function(win){if(win!=front){if(front){front.setActive(false);} +front=win;if(win){win.setActive(true);}}};var activateLast=function(){for(var i=accessList.length-1;i>=0;--i){if(!accessList[i].hidden){setActiveWin(accessList[i]);return;}} +setActiveWin(null);};return{zseed:9000,register:function(win){if(win.manager){win.manager.unregister(win);} +win.manager=this;list[win.id]=win;accessList.push(win);win.on('hide',activateLast);},unregister:function(win){delete win.manager;delete list[win.id];win.un('hide',activateLast);accessList.remove(win);},get:function(id){return typeof id=="object"?id:list[id];},bringToFront:function(win){win=this.get(win);if(win!=front){win._lastAccess=new Date().getTime();orderWindows();return true;} +return false;},sendToBack:function(win){win=this.get(win);win._lastAccess=-(new Date().getTime());orderWindows();return win;},hideAll:function(){for(var id in list){if(list[id]&&typeof list[id]!="function"&&list[id].isVisible()){list[id].hide();}}},getActive:function(){return front;},getBy:function(fn,scope){var r=[];for(var i=accessList.length-1;i>=0;--i){var win=accessList[i];if(fn.call(scope||win,win)!==false){r.push(win);}} +return r;},each:function(fn,scope){for(var id in list){if(list[id]&&typeof list[id]!="function"){if(fn.call(scope||list[id],list[id])===false){return;}}}}};};Ext.WindowMgr=new Ext.WindowGroup();Ext.MessageBox=function(){var dlg,opt,mask,waitTimer,bodyEl,msgEl,textboxEl,textareaEl,progressBar,pp,iconEl,spacerEl,buttons,activeTextEl,bwidth,bufferIcon='',iconCls='',buttonNames=['ok','yes','no','cancel'];var handleButton=function(button){buttons[button].blur();if(dlg.isVisible()){dlg.hide();handleHide();Ext.callback(opt.fn,opt.scope||window,[button,activeTextEl.dom.value,opt],1);}};var handleHide=function(){if(opt&&opt.cls){dlg.el.removeClass(opt.cls);} +progressBar.reset();};var handleEsc=function(d,k,e){if(opt&&opt.closable!==false){dlg.hide();handleHide();} +if(e){e.stopEvent();}};var updateButtons=function(b){var width=0,cfg;if(!b){Ext.each(buttonNames,function(name){buttons[name].hide();});return width;} +dlg.footer.dom.style.display='';Ext.iterate(buttons,function(name,btn){cfg=b[name];if(cfg){btn.show();btn.setText(Ext.isString(cfg)?cfg:Ext.MessageBox.buttonText[name]);width+=btn.getEl().getWidth()+15;}else{btn.hide();}});return width;};return{getDialog:function(titleText){if(!dlg){var btns=[];buttons={};Ext.each(buttonNames,function(name){btns.push(buttons[name]=new Ext.Button({text:this.buttonText[name],handler:handleButton.createCallback(name),hideMode:'offsets'}));},this);dlg=new Ext.Window({autoCreate:true,title:titleText,resizable:false,constrain:true,constrainHeader:true,minimizable:false,maximizable:false,stateful:false,modal:true,shim:true,buttonAlign:"center",width:400,height:100,minHeight:80,plain:true,footer:true,closable:true,close:function(){if(opt&&opt.buttons&&opt.buttons.no&&!opt.buttons.cancel){handleButton("no");}else{handleButton("cancel");}},fbar:new Ext.Toolbar({items:btns,enableOverflow:false})});dlg.render(document.body);dlg.getEl().addClass('x-window-dlg');mask=dlg.mask;bodyEl=dlg.body.createChild({html:'<div class="ext-mb-icon"></div><div class="ext-mb-content"><span class="ext-mb-text"></span><br /><div class="ext-mb-fix-cursor"><input type="text" class="ext-mb-input" /><textarea class="ext-mb-textarea"></textarea></div></div>'});iconEl=Ext.get(bodyEl.dom.firstChild);var contentEl=bodyEl.dom.childNodes[1];msgEl=Ext.get(contentEl.firstChild);textboxEl=Ext.get(contentEl.childNodes[2].firstChild);textboxEl.enableDisplayMode();textboxEl.addKeyListener([10,13],function(){if(dlg.isVisible()&&opt&&opt.buttons){if(opt.buttons.ok){handleButton("ok");}else if(opt.buttons.yes){handleButton("yes");}}});textareaEl=Ext.get(contentEl.childNodes[2].childNodes[1]);textareaEl.enableDisplayMode();progressBar=new Ext.ProgressBar({renderTo:bodyEl});bodyEl.createChild({cls:'x-clear'});} +return dlg;},updateText:function(text){if(!dlg.isVisible()&&!opt.width){dlg.setSize(this.maxWidth,100);} +msgEl.update(text?text+' ':' ');var iw=iconCls!=''?(iconEl.getWidth()+iconEl.getMargins('lr')):0,mw=msgEl.getWidth()+msgEl.getMargins('lr'),fw=dlg.getFrameWidth('lr'),bw=dlg.body.getFrameWidth('lr'),w;w=Math.max(Math.min(opt.width||iw+mw+fw+bw,opt.maxWidth||this.maxWidth),Math.max(opt.minWidth||this.minWidth,bwidth||0));if(opt.prompt===true){activeTextEl.setWidth(w-iw-fw-bw);} +if(opt.progress===true||opt.wait===true){progressBar.setSize(w-iw-fw-bw);} +if(Ext.isIE&&w==bwidth){w+=4;} +msgEl.update(text||' ');dlg.setSize(w,'auto').center();return this;},updateProgress:function(value,progressText,msg){progressBar.updateProgress(value,progressText);if(msg){this.updateText(msg);} +return this;},isVisible:function(){return dlg&&dlg.isVisible();},hide:function(){var proxy=dlg?dlg.activeGhost:null;if(this.isVisible()||proxy){dlg.hide();handleHide();if(proxy){dlg.unghost(false,false);}} +return this;},show:function(options){if(this.isVisible()){this.hide();} +opt=options;var d=this.getDialog(opt.title||" ");d.setTitle(opt.title||" ");var allowClose=(opt.closable!==false&&opt.progress!==true&&opt.wait!==true);d.tools.close.setDisplayed(allowClose);activeTextEl=textboxEl;opt.prompt=opt.prompt||(opt.multiline?true:false);if(opt.prompt){if(opt.multiline){textboxEl.hide();textareaEl.show();textareaEl.setHeight(Ext.isNumber(opt.multiline)?opt.multiline:this.defaultTextHeight);activeTextEl=textareaEl;}else{textboxEl.show();textareaEl.hide();}}else{textboxEl.hide();textareaEl.hide();} +activeTextEl.dom.value=opt.value||"";if(opt.prompt){d.focusEl=activeTextEl;}else{var bs=opt.buttons;var db=null;if(bs&&bs.ok){db=buttons["ok"];}else if(bs&&bs.yes){db=buttons["yes"];} +if(db){d.focusEl=db;}} +if(Ext.isDefined(opt.iconCls)){d.setIconClass(opt.iconCls);} +this.setIcon(Ext.isDefined(opt.icon)?opt.icon:bufferIcon);bwidth=updateButtons(opt.buttons);progressBar.setVisible(opt.progress===true||opt.wait===true);this.updateProgress(0,opt.progressText);this.updateText(opt.msg);if(opt.cls){d.el.addClass(opt.cls);} +d.proxyDrag=opt.proxyDrag===true;d.modal=opt.modal!==false;d.mask=opt.modal!==false?mask:false;if(!d.isVisible()){document.body.appendChild(dlg.el.dom);d.setAnimateTarget(opt.animEl);d.on('show',function(){if(allowClose===true){d.keyMap.enable();}else{d.keyMap.disable();}},this,{single:true});d.show(opt.animEl);} +if(opt.wait===true){progressBar.wait(opt.waitConfig);} +return this;},setIcon:function(icon){if(!dlg){bufferIcon=icon;return;} +bufferIcon=undefined;if(icon&&icon!=''){iconEl.removeClass('x-hidden');iconEl.replaceClass(iconCls,icon);bodyEl.addClass('x-dlg-icon');iconCls=icon;}else{iconEl.replaceClass(iconCls,'x-hidden');bodyEl.removeClass('x-dlg-icon');iconCls='';} +return this;},progress:function(title,msg,progressText){this.show({title:title,msg:msg,buttons:false,progress:true,closable:false,minWidth:this.minProgressWidth,progressText:progressText});return this;},wait:function(msg,title,config){this.show({title:title,msg:msg,buttons:false,closable:false,wait:true,modal:true,minWidth:this.minProgressWidth,waitConfig:config});return this;},alert:function(title,msg,fn,scope){this.show({title:title,msg:msg,buttons:this.OK,fn:fn,scope:scope,minWidth:this.minWidth});return this;},confirm:function(title,msg,fn,scope){this.show({title:title,msg:msg,buttons:this.YESNO,fn:fn,scope:scope,icon:this.QUESTION,minWidth:this.minWidth});return this;},prompt:function(title,msg,fn,scope,multiline,value){this.show({title:title,msg:msg,buttons:this.OKCANCEL,fn:fn,minWidth:this.minPromptWidth,scope:scope,prompt:true,multiline:multiline,value:value});return this;},OK:{ok:true},CANCEL:{cancel:true},OKCANCEL:{ok:true,cancel:true},YESNO:{yes:true,no:true},YESNOCANCEL:{yes:true,no:true,cancel:true},INFO:'ext-mb-info',WARNING:'ext-mb-warning',QUESTION:'ext-mb-question',ERROR:'ext-mb-error',defaultTextHeight:75,maxWidth:600,minWidth:100,minProgressWidth:250,minPromptWidth:250,buttonText:{ok:"OK",cancel:"Cancel",yes:"Yes",no:"No"}};}();Ext.Msg=Ext.MessageBox;Ext.dd.PanelProxy=Ext.extend(Object,{constructor:function(panel,config){this.panel=panel;this.id=this.panel.id+'-ddproxy';Ext.apply(this,config);},insertProxy:true,setStatus:Ext.emptyFn,reset:Ext.emptyFn,update:Ext.emptyFn,stop:Ext.emptyFn,sync:Ext.emptyFn,getEl:function(){return this.ghost;},getGhost:function(){return this.ghost;},getProxy:function(){return this.proxy;},hide:function(){if(this.ghost){if(this.proxy){this.proxy.remove();delete this.proxy;} +this.panel.el.dom.style.display='';this.ghost.remove();delete this.ghost;}},show:function(){if(!this.ghost){this.ghost=this.panel.createGhost(this.panel.initialConfig.cls,undefined,Ext.getBody());this.ghost.setXY(this.panel.el.getXY());if(this.insertProxy){this.proxy=this.panel.el.insertSibling({cls:'x-panel-dd-spacer'});this.proxy.setSize(this.panel.getSize());} +this.panel.el.dom.style.display='none';}},repair:function(xy,callback,scope){this.hide();if(typeof callback=="function"){callback.call(scope||this);}},moveProxy:function(parentNode,before){if(this.proxy){parentNode.insertBefore(this.proxy.dom,before);}}});Ext.Panel.DD=Ext.extend(Ext.dd.DragSource,{constructor:function(panel,cfg){this.panel=panel;this.dragData={panel:panel};this.proxy=new Ext.dd.PanelProxy(panel,cfg);Ext.Panel.DD.superclass.constructor.call(this,panel.el,cfg);var h=panel.header,el=panel.body;if(h){this.setHandleElId(h.id);el=panel.header;} +el.setStyle('cursor','move');this.scroll=false;},showFrame:Ext.emptyFn,startDrag:Ext.emptyFn,b4StartDrag:function(x,y){this.proxy.show();},b4MouseDown:function(e){var x=e.getPageX(),y=e.getPageY();this.autoOffset(x,y);},onInitDrag:function(x,y){this.onStartDrag(x,y);return true;},createFrame:Ext.emptyFn,getDragEl:function(e){return this.proxy.ghost.dom;},endDrag:function(e){this.proxy.hide();this.panel.saveState();},autoOffset:function(x,y){x-=this.startPageX;y-=this.startPageY;this.setDelta(x,y);}});Ext.Toolbar=function(config){if(Ext.isArray(config)){config={items:config,layout:'toolbar'};}else{config=Ext.apply({layout:'toolbar'},config);if(config.buttons){config.items=config.buttons;}} +Ext.Toolbar.superclass.constructor.call(this,config);};(function(){var T=Ext.Toolbar;Ext.extend(T,Ext.Container,{defaultType:'button',enableOverflow:false,trackMenus:true,internalDefaults:{removeMode:'container',hideParent:true},toolbarCls:'x-toolbar',initComponent:function(){T.superclass.initComponent.call(this);this.addEvents('overflowchange');},onRender:function(ct,position){if(!this.el){if(!this.autoCreate){this.autoCreate={cls:this.toolbarCls+' x-small-editor'};} +this.el=ct.createChild(Ext.apply({id:this.id},this.autoCreate),position);Ext.Toolbar.superclass.onRender.apply(this,arguments);}},lookupComponent:function(c){if(Ext.isString(c)){if(c=='-'){c=new T.Separator();}else if(c==' '){c=new T.Spacer();}else if(c=='->'){c=new T.Fill();}else{c=new T.TextItem(c);} +this.applyDefaults(c);}else{if(c.isFormField||c.render){c=this.createComponent(c);}else if(c.tag){c=new T.Item({autoEl:c});}else if(c.tagName){c=new T.Item({el:c});}else if(Ext.isObject(c)){c=c.xtype?this.createComponent(c):this.constructButton(c);}} +return c;},applyDefaults:function(c){if(!Ext.isString(c)){c=Ext.Toolbar.superclass.applyDefaults.call(this,c);var d=this.internalDefaults;if(c.events){Ext.applyIf(c.initialConfig,d);Ext.apply(c,d);}else{Ext.applyIf(c,d);}} +return c;},addSeparator:function(){return this.add(new T.Separator());},addSpacer:function(){return this.add(new T.Spacer());},addFill:function(){this.add(new T.Fill());},addElement:function(el){return this.addItem(new T.Item({el:el}));},addItem:function(item){return this.add.apply(this,arguments);},addButton:function(config){if(Ext.isArray(config)){var buttons=[];for(var i=0,len=config.length;i<len;i++){buttons.push(this.addButton(config[i]));} +return buttons;} +return this.add(this.constructButton(config));},addText:function(text){return this.addItem(new T.TextItem(text));},addDom:function(config){return this.add(new T.Item({autoEl:config}));},addField:function(field){return this.add(field);},insertButton:function(index,item){if(Ext.isArray(item)){var buttons=[];for(var i=0,len=item.length;i<len;i++){buttons.push(this.insertButton(index+i,item[i]));} +return buttons;} +return Ext.Toolbar.superclass.insert.call(this,index,item);},trackMenu:function(item,remove){if(this.trackMenus&&item.menu){var method=remove?'mun':'mon';this[method](item,'menutriggerover',this.onButtonTriggerOver,this);this[method](item,'menushow',this.onButtonMenuShow,this);this[method](item,'menuhide',this.onButtonMenuHide,this);}},constructButton:function(item){var b=item.events?item:this.createComponent(item,item.split?'splitbutton':this.defaultType);return b;},onAdd:function(c){Ext.Toolbar.superclass.onAdd.call(this);this.trackMenu(c);if(this.disabled){c.disable();}},onRemove:function(c){Ext.Toolbar.superclass.onRemove.call(this);if(c==this.activeMenuBtn){delete this.activeMenuBtn;} +this.trackMenu(c,true);},onDisable:function(){this.items.each(function(item){if(item.disable){item.disable();}});},onEnable:function(){this.items.each(function(item){if(item.enable){item.enable();}});},onButtonTriggerOver:function(btn){if(this.activeMenuBtn&&this.activeMenuBtn!=btn){this.activeMenuBtn.hideMenu();btn.showMenu();this.activeMenuBtn=btn;}},onButtonMenuShow:function(btn){this.activeMenuBtn=btn;},onButtonMenuHide:function(btn){delete this.activeMenuBtn;}});Ext.reg('toolbar',Ext.Toolbar);T.Item=Ext.extend(Ext.BoxComponent,{hideParent:true,enable:Ext.emptyFn,disable:Ext.emptyFn,focus:Ext.emptyFn});Ext.reg('tbitem',T.Item);T.Separator=Ext.extend(T.Item,{onRender:function(ct,position){this.el=ct.createChild({tag:'span',cls:'xtb-sep'},position);}});Ext.reg('tbseparator',T.Separator);T.Spacer=Ext.extend(T.Item,{onRender:function(ct,position){this.el=ct.createChild({tag:'div',cls:'xtb-spacer',style:this.width?'width:'+this.width+'px':''},position);}});Ext.reg('tbspacer',T.Spacer);T.Fill=Ext.extend(T.Item,{render:Ext.emptyFn,isFill:true});Ext.reg('tbfill',T.Fill);T.TextItem=Ext.extend(T.Item,{constructor:function(config){T.TextItem.superclass.constructor.call(this,Ext.isString(config)?{text:config}:config);},onRender:function(ct,position){this.autoEl={cls:'xtb-text',html:this.text||''};T.TextItem.superclass.onRender.call(this,ct,position);},setText:function(t){if(this.rendered){this.el.update(t);}else{this.text=t;}}});Ext.reg('tbtext',T.TextItem);T.Button=Ext.extend(Ext.Button,{});T.SplitButton=Ext.extend(Ext.SplitButton,{});Ext.reg('tbbutton',T.Button);Ext.reg('tbsplit',T.SplitButton);})();Ext.ButtonGroup=Ext.extend(Ext.Panel,{baseCls:'x-btn-group',layout:'table',defaultType:'button',frame:true,internalDefaults:{removeMode:'container',hideParent:true},initComponent:function(){this.layoutConfig=this.layoutConfig||{};Ext.applyIf(this.layoutConfig,{columns:this.columns});if(!this.title){this.addClass('x-btn-group-notitle');} +this.on('afterlayout',this.onAfterLayout,this);Ext.ButtonGroup.superclass.initComponent.call(this);},applyDefaults:function(c){c=Ext.ButtonGroup.superclass.applyDefaults.call(this,c);var d=this.internalDefaults;if(c.events){Ext.applyIf(c.initialConfig,d);Ext.apply(c,d);}else{Ext.applyIf(c,d);} +return c;},onAfterLayout:function(){var bodyWidth=this.body.getFrameWidth('lr')+this.body.dom.firstChild.offsetWidth;this.body.setWidth(bodyWidth);this.el.setWidth(bodyWidth+this.getFrameWidth());}});Ext.reg('buttongroup',Ext.ButtonGroup);(function(){var T=Ext.Toolbar;Ext.PagingToolbar=Ext.extend(Ext.Toolbar,{pageSize:20,displayMsg:'Displaying {0} - {1} of {2}',emptyMsg:'No data to display',beforePageText:'Page',afterPageText:'of {0}',firstText:'First Page',prevText:'Previous Page',nextText:'Next Page',lastText:'Last Page',refreshText:'Refresh',initComponent:function(){var pagingItems=[this.first=new T.Button({tooltip:this.firstText,overflowText:this.firstText,iconCls:'x-tbar-page-first',disabled:true,handler:this.moveFirst,scope:this}),this.prev=new T.Button({tooltip:this.prevText,overflowText:this.prevText,iconCls:'x-tbar-page-prev',disabled:true,handler:this.movePrevious,scope:this}),'-',this.beforePageText,this.inputItem=new Ext.form.NumberField({cls:'x-tbar-page-number',allowDecimals:false,allowNegative:false,enableKeyEvents:true,selectOnFocus:true,submitValue:false,listeners:{scope:this,keydown:this.onPagingKeyDown,blur:this.onPagingBlur}}),this.afterTextItem=new T.TextItem({text:String.format(this.afterPageText,1)}),'-',this.next=new T.Button({tooltip:this.nextText,overflowText:this.nextText,iconCls:'x-tbar-page-next',disabled:true,handler:this.moveNext,scope:this}),this.last=new T.Button({tooltip:this.lastText,overflowText:this.lastText,iconCls:'x-tbar-page-last',disabled:true,handler:this.moveLast,scope:this}),'-',this.refresh=new T.Button({tooltip:this.refreshText,overflowText:this.refreshText,iconCls:'x-tbar-loading',handler:this.doRefresh,scope:this})];var userItems=this.items||this.buttons||[];if(this.prependButtons){this.items=userItems.concat(pagingItems);}else{this.items=pagingItems.concat(userItems);} +delete this.buttons;if(this.displayInfo){this.items.push('->');this.items.push(this.displayItem=new T.TextItem({}));} +Ext.PagingToolbar.superclass.initComponent.call(this);this.addEvents('change','beforechange');this.on('afterlayout',this.onFirstLayout,this,{single:true});this.cursor=0;this.bindStore(this.store,true);},onFirstLayout:function(){if(this.dsLoaded){this.onLoad.apply(this,this.dsLoaded);}},updateInfo:function(){if(this.displayItem){var count=this.store.getCount();var msg=count==0?this.emptyMsg:String.format(this.displayMsg,this.cursor+1,this.cursor+count,this.store.getTotalCount());this.displayItem.setText(msg);}},onLoad:function(store,r,o){if(!this.rendered){this.dsLoaded=[store,r,o];return;} +var p=this.getParams();this.cursor=(o.params&&o.params[p.start])?o.params[p.start]:0;var d=this.getPageData(),ap=d.activePage,ps=d.pages;this.afterTextItem.setText(String.format(this.afterPageText,d.pages));this.inputItem.setValue(ap);this.first.setDisabled(ap==1);this.prev.setDisabled(ap==1);this.next.setDisabled(ap==ps);this.last.setDisabled(ap==ps);this.refresh.enable();this.updateInfo();this.fireEvent('change',this,d);},getPageData:function(){var total=this.store.getTotalCount();return{total:total,activePage:Math.ceil((this.cursor+this.pageSize)/this.pageSize),pages:total<this.pageSize?1:Math.ceil(total/this.pageSize)};},changePage:function(page){this.doLoad(((page-1)*this.pageSize).constrain(0,this.store.getTotalCount()));},onLoadError:function(){if(!this.rendered){return;} +this.refresh.enable();},readPage:function(d){var v=this.inputItem.getValue(),pageNum;if(!v||isNaN(pageNum=parseInt(v,10))){this.inputItem.setValue(d.activePage);return false;} +return pageNum;},onPagingFocus:function(){this.inputItem.select();},onPagingBlur:function(e){this.inputItem.setValue(this.getPageData().activePage);},onPagingKeyDown:function(field,e){var k=e.getKey(),d=this.getPageData(),pageNum;if(k==e.RETURN){e.stopEvent();pageNum=this.readPage(d);if(pageNum!==false){pageNum=Math.min(Math.max(1,pageNum),d.pages)-1;this.doLoad(pageNum*this.pageSize);}}else if(k==e.HOME||k==e.END){e.stopEvent();pageNum=k==e.HOME?1:d.pages;field.setValue(pageNum);}else if(k==e.UP||k==e.PAGEUP||k==e.DOWN||k==e.PAGEDOWN){e.stopEvent();if((pageNum=this.readPage(d))){var increment=e.shiftKey?10:1;if(k==e.DOWN||k==e.PAGEDOWN){increment*=-1;} +pageNum+=increment;if(pageNum>=1&pageNum<=d.pages){field.setValue(pageNum);}}}},getParams:function(){return this.paramNames||this.store.paramNames;},beforeLoad:function(){if(this.rendered&&this.refresh){this.refresh.disable();}},doLoad:function(start){var o={},pn=this.getParams();o[pn.start]=start;o[pn.limit]=this.pageSize;if(this.fireEvent('beforechange',this,o)!==false){this.store.load({params:o});}},moveFirst:function(){this.doLoad(0);},movePrevious:function(){this.doLoad(Math.max(0,this.cursor-this.pageSize));},moveNext:function(){this.doLoad(this.cursor+this.pageSize);},moveLast:function(){var total=this.store.getTotalCount(),extra=total%this.pageSize;this.doLoad(extra?(total-extra):total-this.pageSize);},doRefresh:function(){this.doLoad(this.cursor);},bindStore:function(store,initial){var doLoad;if(!initial&&this.store){if(store!==this.store&&this.store.autoDestroy){this.store.destroy();}else{this.store.un('beforeload',this.beforeLoad,this);this.store.un('load',this.onLoad,this);this.store.un('exception',this.onLoadError,this);} +if(!store){this.store=null;}} +if(store){store=Ext.StoreMgr.lookup(store);store.on({scope:this,beforeload:this.beforeLoad,load:this.onLoad,exception:this.onLoadError});doLoad=true;} +this.store=store;if(doLoad){this.onLoad(store,null,{});}},unbind:function(store){this.bindStore(null);},bind:function(store){this.bindStore(store);},onDestroy:function(){this.bindStore(null);Ext.PagingToolbar.superclass.onDestroy.call(this);}});})();Ext.reg('paging',Ext.PagingToolbar);
\ No newline at end of file diff --git a/modules/organize/views/organize_dialog.html.php b/modules/organize/views/organize_dialog.html.php new file mode 100644 index 0000000..9ea4d92 --- /dev/null +++ b/modules/organize/views/organize_dialog.html.php @@ -0,0 +1,23 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<link rel="stylesheet" type="text/css" href="<?= url::file("modules/organize/css/organize_dialog.css") ?>" /> +<script type="text/javascript"> + var ORGANIZE_TITLE = + <?= t("Organize :: %album_title", array("album_title" => "__TITLE__"))->for_js() ?>; + var done_organizing = function(album_id) { + $("#g-dialog").dialog("close"); + window.location = '<?= url::site("items/__ID__") ?>'.replace("__ID__", album_id); + } + + var set_title = function(title) { + $("#g-dialog").dialog("option", "title", ORGANIZE_TITLE.replace("__TITLE__", title)); + } + set_title("<?= html::clean($album->title) ?>"); + + var done_loading = function() { + $("#g-organize-app-loading").hide(); + } +</script> +<div id="g-organize-app-loading"> </div> +<iframe id="g-organize-frame" src="<?= url::site("organize/frame/{$album->id}") ?>"> +</iframe> + diff --git a/modules/organize/views/organize_frame.html.php b/modules/organize/views/organize_frame.html.php new file mode 100644 index 0000000..3a70ccf --- /dev/null +++ b/modules/organize/views/organize_frame.html.php @@ -0,0 +1,604 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<link rel="stylesheet" type="text/css" href="<?= url::file("modules/organize/vendor/ext/css/ext-all.css") ?>" /> +<link rel="stylesheet" type="text/css" href="<?= url::file("modules/organize/vendor/ext/css/ux-all.css") ?>" /> +<link rel="stylesheet" type="text/css" href="<?= url::file("modules/organize/css/organize_frame.css") ?>" /> +<style type="text/css"> + .g-organize div.thumb-album div.icon { + background-image: url(<?= url::file("modules/organize/vendor/ext/images/default/tree/folder.gif") ?>); + } +</style> +<script type="text/javascript" src="<?= url::file("modules/organize/vendor/ext/js/ext-organize-bundle.js") ?>"></script> +<script type="text/javascript"> + Ext.BLANK_IMAGE_URL = "<?= url::file("modules/organize/vendor/ext/images/default/s.gif") ?>"; + Ext.Ajax.timeout = 1000000; // something really large + // I18N for dialog boxes. + Ext.Msg.buttonText = { + ok: <?= t("OK")->for_js() ?>, + cancel: <?= t("Cancel")->for_js() ?>, + yes: <?= t("Yes")->for_js() ?>, + no: <?= t("No")->for_js() ?> + }; + + Ext.onReady(function() { + /* + * ******************************************************************************** + * Utility functions for loading data and making changes + * ******************************************************************************** + */ + var start_busy = function(msg) { + thumb_data_view.el.mask(msg, "loading"); + } + + var stop_busy = function() { + thumb_data_view.el.unmask(); + } + + // Notify the parent dialog that the ExtJS app is loaded + if (parent.done_loading) { + parent.done_loading(); + } + + var show_generic_error = function() { + stop_busy(); + Ext.Msg.alert( + <?= t("An error occurred. Consult your system administrator.")->for_js() ?>); + } + + var current_album_id = null; + var current_album_editable = null; + var load_album_data = function(id) { + if (current_album_id) { + // Don't show the loading message on the initial load, it + // feels a little jarring. + start_busy(<?= t("Loading...")->for_js() ?>); + } + Ext.Ajax.request({ + url: '<?= url::site("organize/album_info/__ID__") ?>'.replace("__ID__", id), + success: function(xhr, opts) { + stop_busy(); + var album_info = Ext.util.JSON.decode(xhr.responseText); + var store = new Ext.data.JsonStore({ + autoDestroy: true, + fields: ["id", "thumb_url", "width", "height", "type", "title"], + idProperty: "id", + root: "children", + data: album_info + }); + current_album_id = id; + thumb_data_view.bindStore(store); + sort_column_combobox.setValue(album_info.sort_column); + sort_order_combobox.setValue(album_info.sort_order); + + current_album_editable = album_info.editable; + if (current_album_editable) { + thumb_data_view.dragZone.unlock(); + } else { + thumb_data_view.dragZone.lock(); + } + if (parent.set_title) { + parent.set_title(album_info.title); + } + }, + failure: show_generic_error + }); + }; + + var reload_album_data = function() { + if (current_album_id) { + load_album_data(current_album_id); + } + }; + + var set_album_sort = function(params) { + start_busy(<?= t("Changing sort...")->for_js() ?>); + params["csrf"] = '<?= access::csrf_token() ?>'; + Ext.Ajax.request({ + url: '<?= url::site("organize/set_sort/__ID__") ?>'.replace("__ID__", current_album_id), + method: "post", + success: function() { + stop_busy(); + reload_album_data(); + }, + failure: show_generic_error, + params: params + }); + } + + var tag_selected_items = function(tag) { + var nodes = thumb_data_view.getSelectedNodes(); + var item_ids = []; + for (var i = 0; i != nodes.length; i++) { + var node = Ext.fly(nodes[i]); + item_ids.push(get_id_from_node(node)); + } + start_busy(<?= t("Tagging...")->for_js() ?>); + Ext.Ajax.request({ + url: '<?= url::site("organize/tag") ?>', + method: "post", + success: function() { + stop_busy(); + reload_album_data(); + }, + failure: show_generic_error, + params: { + item_ids: item_ids.join(","), + tag_names: tag, + csrf: '<?= access::csrf_token() ?>' + } + }); + }; + + var delete_selected_items = function() { + var nodes = thumb_data_view.getSelectedNodes(); + var item_ids = []; + for (var i = 0; i != nodes.length; i++) { + var node = Ext.fly(nodes[i]); + item_ids.push(get_id_from_node(node)); + } + start_busy(<?= t("Deleting...")->for_js() ?>); + Ext.Ajax.request({ + url: '<?= url::site("organize/delete") ?>', + method: "post", + success: function() { + stop_busy(); + reload_album_data(); + }, + failure: show_generic_error, + params: { + item_ids: item_ids.join(","), + csrf: '<?= access::csrf_token() ?>' + } + }); + }; + + var get_id_from_node = function(node) { + var id = node.getAttribute("rel"); + if (id) { + return id; + } + + // IE9 has a bug which causes it to be unable to read the "rel" attr + // so hack it by extracting the id the CSS class id. This is fragile. + // ref: https://sourceforge.net/apps/trac/gallery/ticket/1790 + return node.getAttribute("id").replace("thumb-", ""); + } + + /* + * ******************************************************************************** + * JsonStore, DataView and Panel for viewing albums + * ******************************************************************************** + */ + var thumb_data_view = new Ext.DataView({ + autoScroll: true, + enableDragDrop: true, + itemSelector: "div.thumb", + plugins: [ + new Ext.DataView.DragSelector({dragSafe: true}) + ], + listeners: { + "dblclick": function(v, index, node, e) { + node = Ext.get(node); + if (node.hasClass("thumb-album")) { + var id = get_id_from_node(node); + tree_panel.fireEvent("click", tree_panel.getNodeById(id)) + } + }, + "render": function(v) { + v.dragZone = new Ext.dd.DragZone(v.getEl(), { + ddGroup: "organizeDD", + containerScroll: true, + getDragData: function(e) { + var target = e.getTarget(v.itemSelector, 10); + if (target) { + if (e.ctrlKey == false) { + if (!v.isSelected(target)) { + v.onClick(e); + } + } + var selected_nodes = v.getSelectedNodes(); + var drag_data = { + nodes: selected_nodes, + repair_xy: Ext.fly(target).getXY() + }; + if (selected_nodes.length == 1) { + drag_data.ddel = target; + } else { + var drag_ghost = document.createElement("div"); + drag_ghost.className = "drag-ghost"; + for (var i = 0; i != selected_nodes.length; i++) { + var inner = document.createElement("div"); + drag_ghost.appendChild(inner); + + var img = Ext.get(selected_nodes[i]).dom.firstChild; + var child = inner.appendChild(img.cloneNode(true)); + Ext.get(child).setWidth(Ext.fly(img).getWidth() / 2); + Ext.get(child).setHeight(Ext.fly(img).getHeight() / 2); + } + // The contents of the ghost float, and the ghost is wide enough for + // 4 images across so make sure that the ghost is tall enough. Thumbnails + // are max 120px high max, and ghost thumbs are half of that, but leave some + // padding because IE is unpredictable. + drag_ghost.style.height = Math.ceil(i/4) * 72 + "px"; + drag_data.ddel = drag_ghost; + } + return drag_data; + } + }, + getRepairXY: function() { + return this.dragData.repair_xy; + } + }); + + v.dropZone = new Ext.dd.DropZone(v.getEl(), { + ddGroup: "organizeDD", + getTargetFromEvent: function(e) { + return e.getTarget("div.thumb", 10); + }, + onNodeOut: function(target, dd, e, data) { + Ext.fly(target).removeClass("active-left"); + Ext.fly(target).removeClass("active-right"); + }, + onNodeOver: function(target, dd, e, data) { + var target_x = Ext.lib.Dom.getX(target); + var target_center = target_x + (target.offsetWidth / 2); + if (Ext.lib.Event.getPageX(e) < target_center) { + Ext.fly(target).addClass("active-left"); + Ext.fly(target).removeClass("active-right"); + this.drop_side = "before"; + } else { + Ext.fly(target).removeClass("active-left"); + Ext.fly(target).addClass("active-right"); + this.drop_side = "after"; + } + return Ext.dd.DropZone.prototype.dropAllowed; + }, + onNodeDrop: function(target, dd, e, data) { + var nodes = data.nodes; + source_ids = []; + for (var i = 0; i != nodes.length; i++) { + source_ids.push(get_id_from_node(Ext.fly(nodes[i]))); + } + start_busy(<?= t("Rearranging...")->for_js() ?>); + target = Ext.fly(target); + Ext.Ajax.request({ + url: '<?= url::site("organize/rearrange") ?>', + method: "post", + success: function() { + stop_busy(); + reload_album_data(); + }, + failure: show_generic_error, + params: { + source_ids: source_ids.join(","), + target_id: get_id_from_node(target), + relative: this.drop_side, // calculated in onNodeOver + csrf: '<?= access::csrf_token() ?>' + } + }); + return true; + } + }); + }, + "selectionchange": function(v, selections) { + tag_button.setDisabled(!selections.length || !current_album_editable || !tag_textfield.getValue()); + delete_button.setDisabled(!selections.length || !current_album_editable); + } + }, + multiSelect: true, + selectedClass: "selected", + tpl: new Ext.XTemplate( + '<tpl for=".">', + '<tpl if="thumb_url">', + '<div class="thumb thumb-{type}" id="thumb-{id}" rel="{id}">', + '<img src="{thumb_url}" width="{width}" height="{height}" title="{title}">', + '<div class="icon"></div>', + '</div>', + '</tpl>', + '<tpl if="!thumb_url">', + '<div class="thumb thumb-missing thumb-{type}" id="thumb-{id}" rel="{id}">', + '<span>' + <?= t("No thumbnail")->for_js() ?> + '</span>', + '<div class="icon"></div>', + '</div>', + '</tpl>', + '</tpl>') + }); + + /* + * ******************************************************************************** + * Toolbar with sort column, sort order, delete and close buttons. + * ******************************************************************************** + */ + + sort_order_data = []; + <? foreach (album::get_sort_order_options() as $key => $value): ?> + sort_order_data.push(["<?= $key ?>", <?= $value->for_js() ?>]); + <? endforeach ?> + var sort_column_combobox = new Ext.form.ComboBox({ + mode: "local", + editable: false, + allowBlank: false, + forceSelection: true, + triggerAction: "all", + flex: 3, + store: new Ext.data.ArrayStore({ + id: 0, + fields: ["key", "value"], + data: sort_order_data + }), + listeners: { + "select": function(combo, record, index) { + set_album_sort({sort_column: record.id}); + } + }, + valueField: "key", + displayField: "value" + }); + + var sort_order_combobox = new Ext.form.ComboBox({ + mode: "local", + editable: false, + allowBlank: false, + forceSelection: true, + triggerAction: "all", + flex: 2, + store: new Ext.data.ArrayStore({ + id: 0, + fields: ["key", "value"], + data: [ + ["ASC", <?= t("Ascending")->for_js() ?>], + ["DESC", <?= t("Descending")->for_js() ?>]] + }), + listeners: { + "select": function(combo, record, index) { + set_album_sort({sort_order: record.id}); + } + }, + valueField: "key", + displayField: "value" + }); + + var tag_textfield = new Ext.form.TextField({ + flex: 4, + enableKeyEvents: true, + listeners: { + "keyup": function(v, e) { + var nodes = thumb_data_view.getSelectedNodes(); + tag_button.setDisabled(!nodes.length || !current_album_editable || !tag_textfield.getValue()); + } + } + }); + + var tag_button = new Ext.Button({ + flex: 2, + text: <?= t("Tag")->for_js() ?>, + cls: "x-btn-text-icon", + id: "tag-button", + disabled: true, + listeners: { + "click": function() { + tag_selected_items(tag_textfield.getValue()); + return true; + } + } + }); + + var delete_button = new Ext.Button({ + flex: 2, + text: <?= t("Delete")->for_js() ?>, + cls: "x-btn-text-icon", + iconCls: "delete", + id: "delete-button", + disabled: true, + listeners: { + "click": function() { + Ext.Msg.show({ + title: <?= t("Are you sure you want to delete the selected items?")->for_js() ?>, + buttons: Ext.Msg.YESNO, + fn: function(buttonId) { + if (buttonId == "yes") { + delete_selected_items(); + } + } + }); + return true; + } + } + }); + + var button_panel = new Ext.Panel({ + layout: "hbox", + region: "south", + height: 24, + layoutConfig: { + align: "stretch" + }, + items: [ + { + xtype: "panel", + layout: "hbox", + width: 300, + items: [ + { + xtype: "label", + cls: "sort", + flex: 2, + text: <?= t("Sort order: ")->for_js() ?> + }, + sort_column_combobox, + sort_order_combobox + ] + }, +<? if (module::is_active("tag")): ?> + { + xtype: "spacer", + flex: 3 + }, + tag_textfield, + tag_button, + { + xtype: "spacer", + flex: 1 + }, +<? else: ?> + { + xtype: "spacer", + flex: 10 + }, +<? endif ?> + delete_button, + { + xtype: "button", + flex: 2, + text: <?= t("Close")->for_js() ?>, + listeners: { + "click": function() { + parent.done_organizing(current_album_id); + } + } + } + ] + }); + + var album_panel = new Ext.Panel({ + layout: "fit", + region: "center", + title: <?= t("Drag and drop photos to re-order or move between albums")->for_js() ?>, + items: [thumb_data_view], + bbar: button_panel + }); + + /* + * ******************************************************************************** + * TreeLoader and TreePanel + * ******************************************************************************** + */ + var tree_loader = new Ext.tree.TreeLoader({ + dataUrl: '<?= url::site("organize/tree/{$album->id}") ?>', + nodeParameter: "root_id", + requestMethod: "post" + }); + + var tree_panel = new Ext.tree.TreePanel({ + useArrows: true, + autoScroll: true, + animate: true, + border: false, + containerScroll: true, + enableDD: true, + dropConfig: { + appendOnly: true, + ddGroup: "organizeDD" + }, + listeners: { + "click": function(node) { + load_album_data(node.id); + if (node.isExpandable() && !node.isExpanded()) { + node.expand(); + } + }, + "afterrender": function(v) { + // Override Ext.tree.TreeDropZone.onNodeOver to change the + // x-tree-drop-ok-append CSS class to be x-dd-drop-ok since + // that connotes "ok" instead of "adding something new" and we're + // moving the item, not adding it. + // + // There's probably a better way of overriding the parent method, but + // my JavaScript-fu is weak. + v.dropZone.super_onNodeOver = v.dropZone.onNodeOver; + v.dropZone.onNodeOver = function(target, dd, e, data) { + var returnCls = this.super_onNodeOver(target, dd, e, data); + if (returnCls == "x-tree-drop-ok-append") { + return "x-dd-drop-ok"; + } + return returnCls; + } + + // Override Ext.tree.TreeDropZone.getDropPoint so that it allows dropping + // on any node. The standard function won't let you drop on leaves, but + // in our model we consider an album without sub-albums a leaf. + v.dropZone.getDropPoint = function(e, n, dd) { + return "append"; + } + + v.dropZone.onNodeDrop = function(target, dd, e, data) { + var nodes = data.nodes; + source_ids = []; + var moving_albums = 0; + for (var i = 0; i != nodes.length; i++) { + var node = Ext.fly(nodes[i]); + source_ids.push(get_id_from_node(node)); + moving_albums |= node.hasClass("thumb-album"); + } + start_busy(<?= t("Moving...")->for_js() ?>); + Ext.Ajax.request({ + url: '<?= url::site("organize/reparent") ?>', + method: "post", + success: function() { + stop_busy(); + reload_album_data(); + + // If we're moving albums around then we need to refresh the tree when we're done + if (moving_albums) { + target.node.reload(); + + // If the target node contains the selected node, then the selected + // node just got strafed by the target's reload and no longer exists, + // so we can't reload it. + var selected_node = v.getNodeById(current_album_id); + if (selected_node) { + selected_node.reload(); + } + } + }, + failure: show_generic_error, + params: { + source_ids: source_ids.join(","), + target_id: target.node.id, + csrf: '<?= access::csrf_token() ?>' + } + }); + return true; + } + } + }, + loader: tree_loader, + + region: "west", + split: true, + minSize: 200, + maxSize: 350, + width: 200, + + root: { + allowDrop: Boolean(<?= access::can("edit", item::root()) ?>), + nodeType: "async", + text: "<?= html::clean(item::root()->title) ?>", + draggable: false, + id: "<?= item::root()->id ?>", + expanded: true + } + }); + + var first_organize_load = true; + tree_loader.addListener("load", function() { + if (first_organize_load) { + tree_panel.getNodeById(<?= $album->id ?>).select(); + load_album_data(<?= $album->id ?>); + first_organize_load = false; + + // This is a hack that allows us to reload tree nodes asynchronously + // even though they came with preloaded hierarchical data from the + // initial tree load. Without this, any nodes that were preloaded + // initially won't refresh if you call node.reload() on them. + tree_loader.doPreload = function() { return false; } + } + }); + tree_panel.getRootNode().expand(); + + var outer = new Ext.Viewport({ + layout: "border", + cls: "g-organize", + items: [tree_panel, album_panel] + }); + }); +</script> diff --git a/modules/recaptcha/controllers/admin_recaptcha.php b/modules/recaptcha/controllers/admin_recaptcha.php new file mode 100644 index 0000000..8ba43bb --- /dev/null +++ b/modules/recaptcha/controllers/admin_recaptcha.php @@ -0,0 +1,61 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Recaptcha_Controller extends Admin_Controller { + public function index() { + $form = recaptcha::get_configure_form(); + if (request::method() == "post") { + // @todo move the "save" part of this into a separate controller function + access::verify_csrf(); + $old_public_key = module::get_var("recaptcha", "public_key"); + $old_private_key = module::get_var("recaptcha", "private_key"); + if ($form->validate()) { + $public_key = $form->configure_recaptcha->public_key->value; + $private_key = $form->configure_recaptcha->private_key->value; + + if ($public_key && $private_key) { + module::set_var("recaptcha", "public_key", $public_key); + module::set_var("recaptcha", "private_key", $private_key); + message::success(t("reCAPTCHA configured!")); + log::success("recaptcha", t("reCAPTCHA public and private keys set")); + url::redirect("admin/recaptcha"); + } else if ($public_key && !$private_key) { + $form->configure_recaptcha->private_key->add_error("invalid"); + } else if ($private_key && !$public_key) { + $form->configure_recaptcha->public_key->add_error("invalid"); + } else { + module::set_var("recaptcha", "public_key", ""); + module::set_var("recaptcha", "private_key", ""); + message::success(t("No keys provided. reCAPTCHA is disabled!")); + log::success("recaptcha", t("reCAPTCHA public and private keys cleared")); + url::redirect("admin/recaptcha"); + } + } + } + + recaptcha::check_config(); + $view = new Admin_View("admin.html"); + $view->page_title = t("reCAPTCHA"); + $view->content = new View("admin_recaptcha.html"); + $view->content->public_key = module::get_var("recaptcha", "public_key"); + $view->content->private_key = module::get_var("recaptcha", "private_key"); + $view->content->form = $form; + print $view; + } +} diff --git a/modules/recaptcha/css/recaptcha.css b/modules/recaptcha/css/recaptcha.css new file mode 100644 index 0000000..27117a5 --- /dev/null +++ b/modules/recaptcha/css/recaptcha.css @@ -0,0 +1,7 @@ +#g-content #g-comments ul li #g-recaptcha { + padding: 0; +} + +#g-content #g-comments ul li #g-recaptcha div { + padding: 0; +} diff --git a/modules/recaptcha/helpers/recaptcha.php b/modules/recaptcha/helpers/recaptcha.php new file mode 100644 index 0000000..cc50d72 --- /dev/null +++ b/modules/recaptcha/helpers/recaptcha.php @@ -0,0 +1,152 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class recaptcha_Core { + static function get_configure_form() { + $form = new Forge("admin/recaptcha", "", "post", array("id" => "g-configure-recaptcha-form")); + $group = $form->group("configure_recaptcha") + ->label(t("Configure reCAPTCHA")); + $group->input("public_key") + ->label(t("Public Key")) + ->value(module::get_var("recaptcha", "public_key")) + ->rules("required") + ->error_messages("required", t("You must enter a public key")) + ->error_messages("invalid", t("This public key is invalid")); + $group->input("private_key") + ->label(t("Private Key")) + ->value(module::get_var("recaptcha", "private_key")) + ->callback("recaptcha::verify_key") + ->error_messages("required", t("You must enter a private key")) + ->error_messages("invalid", t("This private key is invalid")); + + $group->submit("")->value(t("Save")); + $site_domain = urlencode(stripslashes($_SERVER["HTTP_HOST"])); + $form->get_key_url = "http://www.google.com/recaptcha/admin/create?domains=$site_domain&app=Gallery3"; + return $form; + } + + static function check_config() { + $public_key = module::get_var("recaptcha", "public_key"); + $private_key = module::get_var("recaptcha", "private_key"); + if (empty($public_key) || empty($private_key)) { + site_status::warning( + t("reCAPTCHA is not quite ready! Please configure the <a href=\"%url\">reCAPTCHA Keys</a>", + array("url" => html::mark_clean(url::site("admin/recaptcha")))), + "recaptcha_config"); + } else { + site_status::clear("recaptcha_config"); + } + } + + /** + * Verify that the recaptcha key is valid. + * @param string $private_key + * @return boolean + */ + static function verify_key($private_key_input) { + if (!$private_key_input->value) { + $private_key_input->add_error("required", 1); + return; + } + + $remote_ip = Input::instance()->server("REMOTE_ADDR"); + $response = self::_http_post("api-verify.recaptcha.net", "/verify", + array("privatekey" => $private_key_input->value, + "remoteip" => $remote_ip, + "challenge" => "right", + "response" => "wrong")); + + if ($response[1] == "false\ninvalid-site-private-key") { + // This is the only thing I can figure out how to verify. + // See http://recaptcha.net/apidocs/captcha for possible return values + $private_key_input->add_error("invalid", 1); + } + } + + /** + * Form validation call back for captcha validation + * @param string $form + * @return string error message or null + */ + static function is_recaptcha_valid($challenge, $response, $private_key) { + $input = Input::instance(); + $remote_ip = $input->server("REMOTE_ADDR"); + + // discard spam submissions + if (empty($challenge) || empty($response)) { + return "incorrect-captcha-sol"; + } + + $response = self::_http_post("api-verify.recaptcha.net", "/verify", + array("privatekey" => $private_key, + "remoteip" => $remote_ip, + "challenge" => $challenge, + "response" => $response)); + + $answers = explode ("\n", $response [1]); + if (trim ($answers [0]) == "true") { + return null; + } else { + return $answers[1]; + } + } + + /** + * Encodes the given data into a query string format + * @param $data - array of string elements to be encoded + * @return string - encoded request + */ + private static function _encode(array $data){ + $req = array(); + foreach ($data as $key => $value){ + $req[] = "$key=" . urlencode(stripslashes($value)); + } + return implode("&", $req); + } + + /** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ + private static function _http_post($host, $path, $data, $port = 80) { + $req = self::_encode($data); + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + $response = ""; + if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { + throw new Exception("@todo COULD NOT OPEN SOCKET"); + } + fwrite($fs, $http_request); + while (!feof($fs)) { + $response .= fgets($fs, 1160); // One TCP-IP packet + } + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + return $response; + } +} diff --git a/modules/recaptcha/helpers/recaptcha_event.php b/modules/recaptcha/helpers/recaptcha_event.php new file mode 100644 index 0000000..4ccacc7 --- /dev/null +++ b/modules/recaptcha/helpers/recaptcha_event.php @@ -0,0 +1,42 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class recaptcha_event_Core { + static function captcha_protect_form($form) { + if (module::get_var("recaptcha", "public_key")) { + foreach ($form->inputs as $input) { + if ($input instanceof Form_Group) { + $input->recaptcha("recaptcha")->label("")->id("g-recaptcha"); + return; + } + } + + // If we haven't returned yet, then add the captcha at the end of the form + $form->recaptcha("recaptcha")->label("")->id("g-recaptcha"); + } + } + + static function admin_menu($menu, $theme) { + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("recaptcha") + ->label(t("reCAPTCHA")) + ->url(url::site("admin/recaptcha"))); + } +} diff --git a/modules/recaptcha/helpers/recaptcha_installer.php b/modules/recaptcha/helpers/recaptcha_installer.php new file mode 100644 index 0000000..1b526e5 --- /dev/null +++ b/modules/recaptcha/helpers/recaptcha_installer.php @@ -0,0 +1,28 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class recaptcha_installer { + static function activate() { + recaptcha::check_config(); + } + + static function deactivate() { + site_status::clear("recaptcha_config"); + } +} diff --git a/modules/recaptcha/helpers/recaptcha_theme.php b/modules/recaptcha/helpers/recaptcha_theme.php new file mode 100644 index 0000000..36087ff --- /dev/null +++ b/modules/recaptcha/helpers/recaptcha_theme.php @@ -0,0 +1,28 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class recaptcha_theme_Core { + static function head($theme) { + return $theme->css("recaptcha.css"); + } + + static function admin_head($theme) { + return $theme->css("recaptcha.css"); + } +}
\ No newline at end of file diff --git a/modules/recaptcha/libraries/Form_Recaptcha.php b/modules/recaptcha/libraries/Form_Recaptcha.php new file mode 100644 index 0000000..8a9e63a --- /dev/null +++ b/modules/recaptcha/libraries/Form_Recaptcha.php @@ -0,0 +1,67 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Form_Recaptcha_Core extends Form_Input { + private $_error = null; + + protected $data = array( + 'name' => '', + 'value' => '', + ); + + public function __construct($name) { + parent::__construct($name); + $this->error_messages("incorrect-captcha-sol", + t("The values supplied to reCAPTCHA are incorrect.")); + $this->error_messages("invalid-site-private-key", t("The site private key is incorrect.")); + } + + public function render() { + $public_key = module::get_var("recaptcha", "public_key"); + if (empty($public_key)) { + throw new Exception("@todo NEED KEY <a href=\"http://recaptcha.net/api/getkey\">" . + "http://recaptcha.net/api/getkey</a>"); + } + + $view = new View("form_recaptcha.html"); + $view->public_key = $public_key; + return $view; + } + + /** + * Validate this input based on the set rules. + * + * @return bool + */ + public function validate() { + $input = Input::instance(); + $challenge = $input->post("recaptcha_challenge_field", "", true); + $response = $input->post("recaptcha_response_field", "", true); + if (!empty($challenge)) { + $this->_error = recaptcha::is_recaptcha_valid( + $challenge, $response, module::get_var("recaptcha", "private_key")); + if (!empty($this->_error)) { + $this->add_error($this->_error, 1); + } + } + $this->is_valid = empty($this->_error); + return empty($this->_error); + } + +}
\ No newline at end of file diff --git a/modules/recaptcha/module.info b/modules/recaptcha/module.info new file mode 100644 index 0000000..6806bb9 --- /dev/null +++ b/modules/recaptcha/module.info @@ -0,0 +1,7 @@ +name = "reCAPTCHA" +description = "reCAPTCHA displays a graphical verification that protects the input form from abuse from 'bots,' or automated programs usually written to generate spam (http://recaptcha.net)." +version = 1 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:recaptcha" +discuss_url = "http://galleryproject.org/forum_module_recaptcha" diff --git a/modules/recaptcha/views/admin_recaptcha.html.php b/modules/recaptcha/views/admin_recaptcha.html.php new file mode 100644 index 0000000..4f07fef --- /dev/null +++ b/modules/recaptcha/views/admin_recaptcha.html.php @@ -0,0 +1,35 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block"> + <h1> <?= t("reCAPTCHA challenge filtering") ?> </h1> + <p> + <?= t("reCAPTCHA is a free CAPTCHA service that helps to digitize books, newspapers and old time radio shows. In order to use it, you need to sign up for a <a href=\"%domain_url\">reCAPTCHA Public/Private Key pair</a>, which is also free. Once registered, the challenge and response strings are evaluated at <a href=\"%recaptcha_url\">recaptcha.net</a> to determine if the form content has been entered by a bot.", + array("domain_url" => $form->get_key_url, + "recaptcha_url" => html::mark_clean("http://recaptcha.net"))) ?> + </p> + + <div class="g-block-content"> + <?= $form ?> + + <? if ($public_key && $private_key): ?> + <div id="g-admin-recaptcha-test"> + <h2> <?= t("reCAPTCHA test") ?> </h2> + <p> + <?= t("If you see a CAPTCHA form below, then reCAPTCHA is functioning properly.") ?> + </p> + + <div id="g-recaptcha"> + <script type="text/javascript" src="http://api.recaptcha.net/js/recaptcha_ajax.js"></script> + <script type="text/javascript"> + Recaptcha.create("<?= $public_key ?>", "g-recaptcha", { + callback: Recaptcha.focus_response_field, + lang: "en", + custom_translations : { instructions_visual : <?= t("Type words to check:")->for_js() ?>}, + theme: "white" + }); + </script> + </div> + </div> + <? endif ?> + + </div> +</div> diff --git a/modules/recaptcha/views/form_recaptcha.html.php b/modules/recaptcha/views/form_recaptcha.html.php new file mode 100644 index 0000000..7481d00 --- /dev/null +++ b/modules/recaptcha/views/form_recaptcha.html.php @@ -0,0 +1,18 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-recaptcha"></div> +<script type="text/javascript" src="<?= request::protocol() ?>://www.google.com/recaptcha/api/js/recaptcha_ajax.js"> +</script> +<script type="text/javascript"> + setTimeout(function() { + Recaptcha.create( + "<?= $public_key ?>", + "g-recaptcha", + { + theme: "white", + custom_translations : { instructions_visual : <?= t("Type words to check:")->for_js() ?>}, + callback: Recaptcha.focus_response_field + } + ); + }, 500); +</script> + diff --git a/modules/rest/controllers/rest.php b/modules/rest/controllers/rest.php new file mode 100644 index 0000000..b3d59e0 --- /dev/null +++ b/modules/rest/controllers/rest.php @@ -0,0 +1,121 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Rest_Controller extends Controller { + const ALLOW_PRIVATE_GALLERY = true; + + public function index() { + $username = Input::instance()->post("user"); + $password = Input::instance()->post("password"); + + if (empty($username) || auth::too_many_failures($username)) { + throw new Rest_Exception("Forbidden", 403); + } + + $user = identity::lookup_user_by_name($username); + if (empty($user) || !identity::is_correct_password($user, $password)) { + module::event("user_login_failed", $username); + throw new Rest_Exception("Forbidden", 403); + } + + auth::login($user); + + rest::reply(rest::access_key()); + } + + public function reset_api_key_confirm() { + $form = new Forge("rest/reset_api_key", "", "post", array("id" => "g-reset-api-key")); + $group = $form->group("confirm_reset")->label(t("Confirm resetting your REST API key")); + $group->submit("")->value(t("Reset")); + $v = new View("reset_api_key_confirm.html"); + $v->form = $form; + print $v; + } + + public function reset_api_key() { + access::verify_csrf(); + rest::reset_access_key(); + message::success(t("Your REST API key has been reset.")); + json::reply(array("result" => "success")); + } + + public function __call($function, $args) { + try { + $input = Input::instance(); + $request = new stdClass(); + + switch ($method = strtolower($input->server("REQUEST_METHOD"))) { + case "get": + $request->params = (object) $input->get(); + break; + + default: + $request->params = (object) $input->post(); + if (isset($_FILES["file"])) { + $request->file = upload::save("file"); + system::delete_later($request->file); + } + break; + } + + if (isset($request->params->entity)) { + $request->params->entity = json_decode($request->params->entity); + } + if (isset($request->params->members)) { + $request->params->members = json_decode($request->params->members); + } + + $request->method = strtolower($input->server("HTTP_X_GALLERY_REQUEST_METHOD", $method)); + $request->access_key = $input->server("HTTP_X_GALLERY_REQUEST_KEY"); + + if (empty($request->access_key) && !empty($request->params->access_key)) { + $request->access_key = $request->params->access_key; + } + + $request->url = url::abs_current(true); + if ($suffix = Kohana::config('core.url_suffix')) { + $request->url = substr($request->url, 0, strlen($request->url) - strlen($suffix)); + } + + rest::set_active_user($request->access_key); + + $handler_class = "{$function}_rest"; + $handler_method = $request->method; + + if (!class_exists($handler_class) || !method_exists($handler_class, $handler_method)) { + throw new Rest_Exception("Bad Request", 400); + } + + $response = call_user_func(array($handler_class, $handler_method), $request); + if ($handler_method == "post") { + // post methods must return a response containing a URI. + header("HTTP/1.1 201 Created"); + header("Location: {$response['url']}"); + } + rest::reply($response); + } catch (ORM_Validation_Exception $e) { + // Note: this is totally insufficient because it doesn't take into account localization. We + // either need to map the result values to localized strings in the application code, or every + // client needs its own l10n string set. + throw new Rest_Exception("Bad Request", 400, $e->validation->errors()); + } catch (Kohana_404_Exception $e) { + throw new Rest_Exception("Not Found", 404); + } + } +}
\ No newline at end of file diff --git a/modules/rest/helpers/registry_rest.php b/modules/rest/helpers/registry_rest.php new file mode 100644 index 0000000..2b4087a --- /dev/null +++ b/modules/rest/helpers/registry_rest.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class registry_rest_Core { + static function get($request) { + $results = array(); + foreach (module::active() as $module) { + foreach (glob(MODPATH . "{$module->name}/helpers/*_rest.php") as $filename) { + $results[] = str_replace("_rest.php", "", basename($filename)); + } + } + return array_unique($results); + } +} diff --git a/modules/rest/helpers/rest.php b/modules/rest/helpers/rest.php new file mode 100644 index 0000000..c6be1e1 --- /dev/null +++ b/modules/rest/helpers/rest.php @@ -0,0 +1,191 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class rest_Core { + const API_VERSION = "3.0"; + + static function reply($data=array()) { + Session::instance()->abort_save(); + + header("X-Gallery-API-Version: " . rest::API_VERSION); + switch (Input::instance()->get("output", "json")) { + case "json": + json::reply($data); + break; + + case "jsonp": + if (!($callback = Input::instance()->get("callback", ""))) { + throw new Rest_Exception( + "Bad Request", 400, array("errors" => array("callback" => "missing"))); + } + + if (preg_match('/^[$A-Za-z_][0-9A-Za-z_]*$/', $callback) == 1) { + header("Content-type: application/javascript; charset=UTF-8"); + print "$callback(" . json_encode($data) . ")"; + } else { + throw new Rest_Exception( + "Bad Request", 400, array("errors" => array("callback" => "invalid"))); + } + break; + + case "html": + header("Content-type: text/html; charset=UTF-8"); + if ($data) { + $html = preg_replace( + "#([\w]+?://[\w]+[^ \'\"\n\r\t<]*)#ise", "'<a href=\"\\1\" >\\1</a>'", + var_export($data, 1)); + } else { + $html = t("Empty response"); + } + print "<pre>$html</pre>"; + if (gallery::show_profiler()) { + Profiler::enable(); + $profiler = new Profiler(); + $profiler->render(); + } + break; + + default: + throw new Rest_Exception("Bad Request", 400); + } + } + + static function set_active_user($access_key) { + if (empty($access_key)) { + if (module::get_var("rest", "allow_guest_access")) { + identity::set_active_user(identity::guest()); + return; + } else { + throw new Rest_Exception("Forbidden", 403); + } + } + + $key = ORM::factory("user_access_key") + ->where("access_key", "=", $access_key) + ->find(); + + if (!$key->loaded()) { + throw new Rest_Exception("Forbidden", 403); + } + + $user = identity::lookup_user($key->user_id); + if (empty($user)) { + throw new Rest_Exception("Forbidden", 403); + } + + identity::set_active_user($user); + } + + static function reset_access_key() { + $key = ORM::factory("user_access_key") + ->where("user_id", "=", identity::active_user()->id) + ->find(); + if ($key->loaded()) { + $key->delete(); + } + return rest::access_key(); + } + + static function access_key() { + $key = ORM::factory("user_access_key") + ->where("user_id", "=", identity::active_user()->id) + ->find(); + + if (!$key->loaded()) { + $key->user_id = identity::active_user()->id; + $key->access_key = md5(random::hash() . access::private_key()); + $key->save(); + } + + return $key->access_key; + } + + /** + * Convert a REST url into an object. + * Eg: + * http://example.com/gallery3/index.php/rest/item/35 -> Item_Model + * http://example.com/gallery3/index.php/rest/tag/16 -> Tag_Model + * http://example.com/gallery3/index.php/rest/tagged_item/1,16 -> [Tag_Model, Item_Model] + * + * @param string the fully qualified REST url + * @return mixed the corresponding object (usually a model of some kind) + */ + static function resolve($url) { + if ($suffix = Kohana::config('core.url_suffix')) { + $relative_url = substr($url, strlen(url::abs_site("rest")) - strlen($suffix)); + } else { + $relative_url = substr($url, strlen(url::abs_site("rest"))); + } + + $path = parse_url($relative_url, PHP_URL_PATH); + $components = explode("/", $path, 3); + + if (count($components) != 3) { + throw new Kohana_404_Exception($url); + } + + $class = "$components[1]_rest"; + if (!class_exists($class) || !method_exists($class, "resolve")) { + throw new Kohana_404_Exception($url); + } + + return call_user_func(array($class, "resolve"), !empty($components[2]) ? $components[2] : null); + } + + /** + * Return an absolute url used for REST resource location. + * @param string resource type (eg, "item", "tag") + * @param object resource + */ + static function url() { + $args = func_get_args(); + $resource_type = array_shift($args); + + $class = "{$resource_type}_rest"; + if (!class_exists($class) || !method_exists($class, "url")) { + throw new Rest_Exception("Bad Request", 400); + } + + $url = call_user_func_array(array($class, "url"), $args); + if (Input::instance()->get("output") == "html") { + if (strpos($url, "?") === false) { + $url .= "?output=html"; + } else { + $url .= "&output=html"; + } + } + return $url; + } + + static function relationships($resource_type, $resource) { + $results = array(); + foreach (module::active() as $module) { + foreach (glob(MODPATH . "{$module->name}/helpers/*_rest.php") as $filename) { + $class = str_replace(".php", "", basename($filename)); + if (class_exists($class) && method_exists($class, "relationships")) { + if ($tmp = call_user_func(array($class, "relationships"), $resource_type, $resource)) { + $results = array_merge($results, $tmp); + } + } + } + } + + return $results; + } +} diff --git a/modules/rest/helpers/rest_event.php b/modules/rest/helpers/rest_event.php new file mode 100644 index 0000000..991e2b6 --- /dev/null +++ b/modules/rest/helpers/rest_event.php @@ -0,0 +1,102 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class rest_event { + /** + * Called just before a user is deleted. This will remove the user from + * the user_homes directory. + */ + static function user_before_delete($user) { + db::build() + ->delete("user_access_keys") + ->where("id", "=", $user->id) + ->execute(); + } + + + static function change_provider($new_provider) { + db::build() + ->delete("user_access_keys") + ->execute(); + } + + /** + * Called after a user has been added. Just add a remote access key + * on every add. + */ + static function user_add_form_admin_completed($user, $form) { + $key = ORM::factory("user_access_key"); + $key->user_id = $user->id; + $key->access_key = random::hash(); + $key->save(); + } + + /** + * Called when admin is editing a user + */ + static function user_edit_form_admin($user, $form) { + self::_get_access_key_form($user, $form); + } + + /** + * Get the form fields for user edit + */ + static function _get_access_key_form($user, $form) { + $key = ORM::factory("user_access_key") + ->where("user_id", "=", $user->id) + ->find(); + + if (!$key->loaded()) { + $key->user_id = $user->id; + $key->access_key = random::hash(); + $key->save(); + } + + $form->edit_user->input("user_access_key") + ->value($key->access_key) + ->readonly("readonly") + ->class("g-form-static") + ->label(t("Remote access key")); + } + + static function show_user_profile($data) { + // Guests can't see a REST key + if (identity::active_user()->guest) { + return; + } + + // Only logged in users can see their own REST key + if (identity::active_user()->id != $data->user->id) { + return; + } + + $view = new View("user_profile_rest.html"); + $key = ORM::factory("user_access_key") + ->where("user_id", "=", $data->user->id) + ->find(); + + if (!$key->loaded()) { + $key->user_id = $data->user->id; + $key->access_key = random::hash(); + $key->save(); + } + $view->rest_key = $key->access_key; + $data->content[] = (object)array("title" => t("REST API"), "view" => $view); + } +} diff --git a/modules/rest/helpers/rest_installer.php b/modules/rest/helpers/rest_installer.php new file mode 100644 index 0000000..96f8acf --- /dev/null +++ b/modules/rest/helpers/rest_installer.php @@ -0,0 +1,52 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class rest_installer { + static function install() { + Database::instance() + ->query("CREATE TABLE {user_access_keys} ( + `id` int(9) NOT NULL auto_increment, + `user_id` int(9) NOT NULL, + `access_key` char(32) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`access_key`), + UNIQUE KEY(`user_id`)) + DEFAULT CHARSET=utf8;"); + module::set_var("rest", "allow_guest_access", false); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + if (in_array("user_access_tokens", Database::instance()->list_tables())) { + $db->query("RENAME TABLE {user_access_tokens} TO {user_access_keys}"); + } + module::set_version("rest", $version = 2); + } + + if ($version == 2) { + module::set_var("rest", "allow_guest_access", false); + module::set_version("rest", $version = 3); + } + } + + static function uninstall() { + Database::instance()->query("DROP TABLE IF EXISTS {user_access_keys}"); + } +} diff --git a/modules/rest/libraries/Rest_Exception.php b/modules/rest/libraries/Rest_Exception.php new file mode 100644 index 0000000..bd08aa8 --- /dev/null +++ b/modules/rest/libraries/Rest_Exception.php @@ -0,0 +1,37 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Rest_Exception_Core extends Kohana_Exception { + var $response = array(); + + public function __construct($message, $code, $response=array()) { + parent::__construct($message, null, $code); + $this->response = $response; + } + + public function sendHeaders() { + if (!headers_sent()) { + header("HTTP/1.1 " . $this->getCode() . " " . $this->getMessage()); + } + } + + public function getTemplate() { + return "error_rest.json"; + } +}
\ No newline at end of file diff --git a/modules/rest/models/user_access_key.php b/modules/rest/models/user_access_key.php new file mode 100644 index 0000000..d464380 --- /dev/null +++ b/modules/rest/models/user_access_key.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class User_Access_Key_Model_Core extends ORM { +} diff --git a/modules/rest/module.info b/modules/rest/module.info new file mode 100644 index 0000000..93a7873 --- /dev/null +++ b/modules/rest/module.info @@ -0,0 +1,8 @@ +name = "REST API Module" +description = "A REST-based API that allows desktop clients and other apps to interact with Gallery 3" + +version = 3 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:rest" +discuss_url = "http://galleryproject.org/forum_module_rest" diff --git a/modules/rest/views/error_rest.json.php b/modules/rest/views/error_rest.json.php new file mode 100644 index 0000000..8c99ef4 --- /dev/null +++ b/modules/rest/views/error_rest.json.php @@ -0,0 +1,6 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? +// Log error response to ease debugging +Kohana_Log::add("error", "Rest error details: " . print_r($e->response, 1)); +?> +<?= json_encode($e->response);
\ No newline at end of file diff --git a/modules/rest/views/reset_api_key_confirm.html.php b/modules/rest/views/reset_api_key_confirm.html.php new file mode 100644 index 0000000..3aae2a9 --- /dev/null +++ b/modules/rest/views/reset_api_key_confirm.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-rest-reset-api-key" class="ui-helper-clearfix"> + <p> + <?= t("Do you really want to reset your REST API key? Any clients that use this key will need to be updated with the new value.") ?> + </p> + <?= $form ?> +</div> diff --git a/modules/rest/views/user_profile_rest.html.php b/modules/rest/views/user_profile_rest.html.php new file mode 100644 index 0000000..3e5d3db --- /dev/null +++ b/modules/rest/views/user_profile_rest.html.php @@ -0,0 +1,13 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-rest-detail"> + <ul> + <li id="g-rest-key"> + <p> + <?= t("<b>Key</b>: %key", array("key" => $rest_key)) ?> + <a class="g-button ui-state-default ui-corner-all g-dialog-link" href="<?= url::site("rest/reset_api_key_confirm") ?>"> + <?= t("reset") ?> + </a> + </p> + </li> + </ul> +</div> diff --git a/modules/rss/controllers/rss.php b/modules/rss/controllers/rss.php new file mode 100644 index 0000000..571995b --- /dev/null +++ b/modules/rss/controllers/rss.php @@ -0,0 +1,66 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Rss_Controller extends Controller { + public static $page_size = 20; + + public function feed($module_id, $feed_id, $id=null) { + $page = (int) Input::instance()->get("page", 1); + if ($page < 1) { + url::redirect(url::merge(array("page" => 1))); + } + + // Configurable page size between 1 and 100, default 20 + $page_size = max(1, min(100, (int) Input::instance()->get("page_size", self::$page_size))); + + // Run the appropriate feed callback + if (module::is_active($module_id)) { + $class_name = "{$module_id}_rss"; + if (class_exists($class_name) && method_exists($class_name, "feed")) { + $feed = call_user_func( + array($class_name, "feed"), $feed_id, + ($page - 1) * $page_size, $page_size, $id); + } + } + if (empty($feed)) { + throw new Kohana_404_Exception(); + } + + if ($feed->max_pages && $page > $feed->max_pages) { + url::redirect(url::merge(array("page" => $feed->max_pages))); + } + + $view = new View(empty($feed->view) ? "feed.mrss" : $feed->view); + unset($feed->view); + + $view->feed = $feed; + $view->pub_date = date("D, d M Y H:i:s O"); + + $feed->uri = url::abs_site(url::merge($_GET)); + if ($page > 1) { + $feed->previous_page_uri = url::abs_site(url::merge(array("page" => $page - 1))); + } + if ($page < $feed->max_pages) { + $feed->next_page_uri = url::abs_site(url::merge(array("page" => $page + 1))); + } + + header("Content-Type: application/rss+xml"); + print $view; + } +}
\ No newline at end of file diff --git a/modules/rss/helpers/rss.php b/modules/rss/helpers/rss.php new file mode 100644 index 0000000..d6e60f1 --- /dev/null +++ b/modules/rss/helpers/rss.php @@ -0,0 +1,36 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class rss_Core { + /** + * Convert a rss feed id into a rss feed url. + */ + static function url($uri) { + return url::site("rss/feed/$uri"); + } + + /** + * Return a <link> element for a given rss feed id. + */ + static function feed_link($uri) { + $url = url::site("rss/feed/$uri"); + return "<link rel=\"alternate\" type=\"application/rss+xml\" href=\"$url\" />"; + } +}
\ No newline at end of file diff --git a/modules/rss/helpers/rss_block.php b/modules/rss/helpers/rss_block.php new file mode 100644 index 0000000..9a77b05 --- /dev/null +++ b/modules/rss/helpers/rss_block.php @@ -0,0 +1,49 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class rss_block_Core { + static function get_site_list() { + return array("rss_feeds" => t("Available RSS feeds")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "rss_feeds": + $feeds = array(); + foreach (module::active() as $module) { + $class_name = "{$module->name}_rss"; + if (class_exists($class_name) && method_exists($class_name, "available_feeds")) { + $feeds = array_merge($feeds, + call_user_func(array($class_name, "available_feeds"), $theme->item(), $theme->tag())); + } + } + if (!empty($feeds)) { + $block = new Block(); + $block->css_id = "g-rss"; + $block->title = t("Available RSS feeds"); + $block->content = new View("rss_block.html"); + $block->content->feeds = $feeds; + } + break; + } + + return $block; + } +} diff --git a/modules/rss/module.info b/modules/rss/module.info new file mode 100644 index 0000000..5f32387 --- /dev/null +++ b/modules/rss/module.info @@ -0,0 +1,7 @@ +name = "RSS" +description = "Provides RSS feeds" +version = 1 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:rss" +discuss_url = "http://galleryproject.org/forum_module_rss" diff --git a/modules/rss/views/feed.mrss.php b/modules/rss/views/feed.mrss.php new file mode 100644 index 0000000..b609a54 --- /dev/null +++ b/modules/rss/views/feed.mrss.php @@ -0,0 +1,79 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? echo '<?xml version="1.0" ?>' ?> +<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:content="http://purl.org/rss/1.0/modules/content/" + xmlns:fh="http://purl.org/syndication/history/1.0"> + <channel> + <generator>gallery3</generator> + <title><?= html::clean($feed->title) ?></title> + <link><?= $feed->uri ?></link> + <description><?= html::clean($feed->description) ?></description> + <language>en-us</language> + <atom:link rel="self" href="<?= $feed->uri ?>" type="application/rss+xml" /> + <fh:complete/> + <? if (!empty($feed->previous_page_uri)): ?> + <atom:link rel="previous" href="<?= $feed->previous_page_uri ?>" type="application/rss+xml" /> + <? endif ?> + <? if (!empty($feed->next_page_uri)): ?> + <atom:link rel="next" href="<?= $feed->next_page_uri ?>" type="application/rss+xml" /> + <? endif ?> + <pubDate><?= $pub_date ?></pubDate> + <lastBuildDate><?= $pub_date ?></lastBuildDate> + <? foreach ($feed->items as $item): ?> + <item> + <title><?= html::purify($item->title) ?></title> + <link><?= url::abs_site("{$item->type}s/{$item->id}") ?></link> + <guid isPermaLink="true"><?= url::abs_site("{$item->type}s/{$item->id}") ?></guid> + <pubDate><?= date("D, d M Y H:i:s O", $item->created); ?></pubDate> + <description><?= html::purify($item->description) ?></description> + <content:encoded> + <![CDATA[ + <span><?= html::purify($item->description) ?></span> + <p> + <? if ($item->type == "photo"): ?> + <img alt="" src="<?= $item->resize_url(true) ?>" + title="<?= html::purify($item->title)->for_html_attr() ?>" + height="<?= $item->resize_height ?>" width="<?= $item->resize_width ?>" /><br /> + <? else: ?> + <a href="<?= url::abs_site("{$item->type}s/{$item->id}") ?>"> + <img alt="" src="<?= $item->thumb_url(true) ?>" + title="<?= html::purify($item->title)->for_html_attr() ?>" + height="<?= $item->thumb_height ?>" width="<?= $item->thumb_width ?>" /></a><br /> + <? endif ?> + <?= html::purify($item->description) ?> + </p> + ]]> + </content:encoded> + <media:thumbnail url="<?= $item->thumb_url(true) ?>" + height="<?= $item->thumb_height ?>" + width="<?= $item->thumb_width ?>" + /> + <? $view_full = access::can("view_full", $item); ?> + <? if ($item->type == "photo" && $view_full): ?> + <media:group> + <? endif ?> + <? if ($item->type == "photo"): ?> + <media:content url="<?= $item->resize_url(true) ?>" + fileSize="<?= @filesize($item->resize_path()) ?>" + type="<?= $item->mime_type ?>" + height="<?= $item->resize_height ?>" + width="<?= $item->resize_width ?>" + /> + <? endif ?> + <? if ($view_full): ?> + <media:content url="<?= $item->file_url(true) ?>" + fileSize="<?= @filesize($item->file_path()) ?>" + type="<?= $item->mime_type ?>" + height="<?= $item->height ?>" + width="<?= $item->width ?>" + isDefault="true" + /> + <? endif ?> + <? if ($item->type == "photo" && $view_full): ?> + </media:group> + <? endif ?> + </item> + <? endforeach ?> + </channel> +</rss> diff --git a/modules/rss/views/rss_block.html.php b/modules/rss/views/rss_block.html.php new file mode 100644 index 0000000..210c72a --- /dev/null +++ b/modules/rss/views/rss_block.html.php @@ -0,0 +1,13 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul id="g-feeds"> +<? foreach($feeds as $url => $title): ?> + <li style="clear: both;"> + <span class="ui-icon-left"> + <a href="<?= rss::url($url) ?>"> + <span class="ui-icon ui-icon-signal-diag"></span> + <?= html::purify($title) ?> + </a> + </span> + </li> +<? endforeach ?> +</ul> diff --git a/modules/search/controllers/search.php b/modules/search/controllers/search.php new file mode 100644 index 0000000..753d9b6 --- /dev/null +++ b/modules/search/controllers/search.php @@ -0,0 +1,123 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Search_Controller extends Controller { + public function index() { + $page_size = module::get_var("gallery", "page_size", 9); + $q = Input::instance()->get("q"); + $q_with_more_terms = search::add_query_terms($q); + $show = Input::instance()->get("show"); + + $album_id = Input::instance()->get("album", item::root()->id); + $album = ORM::factory("item", $album_id); + if (!access::can("view", $album) || !$album->is_album()) { + $album = item::root(); + } + + if ($show) { + $child = ORM::factory("item", $show); + $index = search::get_position_within_album($child, $q_with_more_terms, $album); + if ($index) { + $page = ceil($index / $page_size); + url::redirect(url::abs_site("search" . + "?q=" . urlencode($q) . + "&album=" . urlencode($album->id) . + ($page == 1 ? "" : "&page=$page"))); + } + } + + $page = Input::instance()->get("page", 1); + + // Make sure that the page references a valid offset + if ($page < 1) { + $page = 1; + } + + $offset = ($page - 1) * $page_size; + + list ($count, $result) = + search::search_within_album($q_with_more_terms, $album, $page_size, $offset); + + $max_pages = max(ceil($count / $page_size), 1); + + $template = new Theme_View("page.html", "collection", "search"); + $root = item::root(); + $template->set_global( + array("page" => $page, + "max_pages" => $max_pages, + "page_size" => $page_size, + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance($q, url::abs_site("search?q=" . urlencode($q)))->set_last(), + ), + "children_count" => $count)); + + $template->content = new View("search.html"); + $template->content->album = $album; + $template->content->items = $result; + $template->content->q = $q; + + print $template; + + item::set_display_context_callback("Search_Controller::get_display_context", $album, $q); + } + + static function get_display_context($item, $album, $q) { + $q_with_more_terms = search::add_query_terms($q); + $position = search::get_position_within_album($item, $q_with_more_terms, $album); + + if ($position > 1) { + list ($count, $result_data) = + search::search_within_album($q_with_more_terms, $album, 3, $position - 2); + list ($previous_item, $ignore, $next_item) = $result_data; + } else { + $previous_item = null; + list ($count, $result_data) = + search::search_within_album($q_with_more_terms, $album, 1, $position); + list ($next_item) = $result_data; + } + + $search_url = url::abs_site("search" . + "?q=" . urlencode($q) . + "&album=" . urlencode($album->id) . + "&show={$item->id}"); + $root = item::root(); + + return array("position" => $position, + "previous_item" => $previous_item, + "next_item" => $next_item, + "sibling_count" => $count, + "siblings_callback" => array("Search_Controller::get_siblings", array($q, $album)), + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance(t("Search: %q", array("q" => $q)), $search_url), + Breadcrumb::instance($item->title, $item->url())->set_last())); + } + + static function get_siblings($q, $album, $limit, $offset) { + if (!isset($limit)) { + $limit = 100; + } + if (!isset($offset)) { + $offset = 1; + } + $result = search::search_within_album(search::add_query_terms($q), $album, $limit, $offset); + return $result[1]; + } +} diff --git a/modules/search/helpers/search.php b/modules/search/helpers/search.php new file mode 100644 index 0000000..c84b70b --- /dev/null +++ b/modules/search/helpers/search.php @@ -0,0 +1,163 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class search_Core { + /** + * Add more terms to the query by wildcarding the stem value of the first + * few terms in the query. + */ + static function add_query_terms($q) { + $MAX_TERMS = 5; + $terms = explode(" ", $q, $MAX_TERMS); + for ($i = 0; $i < min(count($terms), $MAX_TERMS - 1); $i++) { + // Don't wildcard quoted or already wildcarded terms + if ((substr($terms[$i], 0, 1) != '"') && (substr($terms[$i], -1, 1) != "*")) { + $terms[] = rtrim($terms[$i], "s") . "*"; + } + } + return implode(" ", $terms); + } + + static function search($q, $limit, $offset) { + return search::search_within_album($q, item::root(), $limit, $offset); + } + + static function search_within_album($q, $album, $limit, $offset) { + $db = Database::instance(); + + $query = self::_build_query_base($q, $album) . + " ORDER BY `score` DESC" . + " LIMIT $limit OFFSET " . (int)$offset; + + $data = $db->query($query); + $count = $db->query("SELECT FOUND_ROWS() as c")->current()->c; + + return array($count, new ORM_Iterator(ORM::factory("item"), $data)); + } + + private static function _build_query_base($q, $album, $where=array()) { + $db = Database::instance(); + $q = $db->escape($q); + + if (!identity::active_user()->admin) { + foreach (identity::group_ids_for_active_user() as $id) { + $fields[] = "`view_$id` = TRUE"; // access::ALLOW + } + $access_sql = " AND (" . join(" OR ", $fields) . ")"; + } else { + $access_sql = ""; + } + + if ($album->id == item::root()->id) { + $album_sql = ""; + } else { + $album_sql = + " AND {items}.left_ptr > " . $db->escape($album->left_ptr) . + " AND {items}.right_ptr <= " . $db->escape($album->right_ptr); + } + + return + "SELECT SQL_CALC_FOUND_ROWS {items}.*, " . + " MATCH({search_records}.`data`) AGAINST ('$q') AS `score` " . + "FROM {items} JOIN {search_records} ON ({items}.`id` = {search_records}.`item_id`) " . + "WHERE MATCH({search_records}.`data`) AGAINST ('$q' IN BOOLEAN MODE)" . + $album_sql . + (empty($where) ? "" : " AND " . join(" AND ", $where)) . + $access_sql; + } + + /** + * @return string An error message suitable for inclusion in the task log + */ + static function check_index() { + list ($remaining) = search::stats(); + if ($remaining) { + site_status::warning( + t('Your search index needs to be updated. <a href="%url" class="g-dialog-link">Fix this now</a>', + array("url" => html::mark_clean(url::site("admin/maintenance/start/search_task::update_index?csrf=__CSRF__")))), + "search_index_out_of_date"); + } + } + + static function update($item) { + $data = new ArrayObject(); + $record = ORM::factory("search_record")->where("item_id", "=", $item->id)->find(); + if (!$record->loaded()) { + $record->item_id = $item->id; + } + + $item = $record->item(); + module::event("item_index_data", $item, $data); + $record->data = join(" ", (array)$data); + $record->dirty = 0; + $record->save(); + } + + static function stats() { + $remaining = db::build() + ->from("items") + ->join("search_records", "items.id", "search_records.item_id", "left") + ->and_open() + ->where("search_records.item_id", "IS", null) + ->or_where("search_records.dirty", "=", 1) + ->close() + ->count_records(); + + $total = ORM::factory("item")->count_all(); + $percent = round(100 * ($total - $remaining) / $total); + + return array($remaining, $total, $percent); + } + + static function get_position($item, $q) { + return search::get_position_within_album($item, $q, item::root()); + } + + static function get_position_within_album($item, $q, $album) { + $page_size = module::get_var("gallery", "page_size", 9); + $query = self::_build_query_base($q, $album, array("{items}.id = " . $item->id)) . + " ORDER BY `score` DESC"; + $db = Database::instance(); + + // Truncate the score by two decimal places as this resolves the issues + // that arise due to inexact numeric conversions. + $current = $db->query($query)->current(); + if (!$current) { + // We can't find this result in our result set - perhaps we've fallen out of context? Clear + // the context and try again. + item::clear_display_context_callback(); + url::redirect(url::current()); + } + $score = $current->score; + if (strlen($score) > 7) { + $score = substr($score, 0, strlen($score) - 2); + } + + // Redo the query but only look for results greater than or equal to our current location + // then seek backwards until we find our item. + $data = $db->query(self::_build_query_base($q, $album) . " HAVING `score` >= " . $score . + " ORDER BY `score` DESC"); + $data->seek($data->count() - 1); + + while ($data->get("id") != $item->id && $data->prev()->valid()) { + } + + return $data->key() + 1; + } +} diff --git a/modules/search/helpers/search_event.php b/modules/search/helpers/search_event.php new file mode 100644 index 0000000..a20935b --- /dev/null +++ b/modules/search/helpers/search_event.php @@ -0,0 +1,39 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class search_event_Core { + static function item_created($item) { + search::update($item); + } + + static function item_updated($original, $new) { + search::update($new); + } + + static function item_deleted($item) { + db::build() + ->delete("search_records") + ->where("item_id", "=", $item->id) + ->execute(); + } + + static function item_related_update($item) { + search::update($item); + } +} diff --git a/modules/search/helpers/search_installer.php b/modules/search/helpers/search_installer.php new file mode 100644 index 0000000..c9e8f26 --- /dev/null +++ b/modules/search/helpers/search_installer.php @@ -0,0 +1,50 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class search_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE {search_records} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9), + `dirty` boolean default 1, + `data` LONGTEXT default NULL, + PRIMARY KEY (`id`), + KEY(`item_id`), + FULLTEXT INDEX (`data`)) + ENGINE=MyISAM + DEFAULT CHARSET=utf8;"); + } + + static function activate() { + // Update the root item. This is a quick hack because the search module is activated as part + // of the official install, so this way we don't start off with a "your index is out of date" + // banner. + search::update(model_cache::get("item", 1)); + search::check_index(); + } + + static function deactivate() { + site_status::clear("search_index_out_of_date"); + } + + static function uninstall() { + Database::instance()->query("DROP TABLE {search_records}"); + } +} diff --git a/modules/search/helpers/search_task.php b/modules/search/helpers/search_task.php new file mode 100644 index 0000000..18348a2 --- /dev/null +++ b/modules/search/helpers/search_task.php @@ -0,0 +1,84 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class search_task_Core { + static function available_tasks() { + // Delete extra search_records + db::build() + ->delete("search_records") + ->where("item_id", "NOT IN", db::build()->select("id")->from("items")) + ->execute(); + + list ($remaining, $total, $percent) = search::stats(); + return array(Task_Definition::factory() + ->callback("search_task::update_index") + ->name(t("Update Search Index")) + ->description( + $remaining + ? t2("1 photo or album needs to be scanned", + "%count (%percent%) of your photos and albums need to be scanned", + $remaining, array("percent" => (100 - $percent))) + : t("Search data is up-to-date")) + ->severity($remaining ? log::WARNING : log::SUCCESS)); + } + + static function update_index($task) { + try { + $completed = $task->get("completed", 0); + + $start = microtime(true); + foreach (ORM::factory("item") + ->join("search_records", "items.id", "search_records.item_id", "left") + ->where("search_records.item_id", "IS", null) + ->or_where("search_records.dirty", "=", 1) + ->find_all(100) as $item) { + // The query above can take a long time, so start the timer after its done + // to give ourselves a little time to actually process rows. + if (!isset($start)) { + $start = microtime(true); + } + + search::update($item); + $completed++; + + if (microtime(true) - $start > .75) { + break; + } + } + + list ($remaining, $total, $percent) = search::stats(); + $task->set("completed", $completed); + if ($remaining == 0 || !($remaining + $completed)) { + $task->done = true; + $task->state = "success"; + site_status::clear("search_index_out_of_date"); + $task->percent_complete = 100; + } else { + $task->percent_complete = round(100 * $completed / ($remaining + $completed)); + } + $task->status = t2("one record updated, index is %percent% up-to-date", + "%count records updated, index is %percent% up-to-date", + $completed, array("percent" => $percent)); + } catch (Exception $e) { + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + } + } +} diff --git a/modules/search/helpers/search_theme.php b/modules/search/helpers/search_theme.php new file mode 100644 index 0000000..e8735d1 --- /dev/null +++ b/modules/search/helpers/search_theme.php @@ -0,0 +1,29 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class search_theme_Core { + static function header_top($theme) { + if ($theme->page_subtype() != "login") { + $view = new View("search_link.html"); + return $view->render(); + } else { + return ""; + } + } +}
\ No newline at end of file diff --git a/modules/search/models/search_record.php b/modules/search/models/search_record.php new file mode 100644 index 0000000..be68e72 --- /dev/null +++ b/modules/search/models/search_record.php @@ -0,0 +1,24 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Search_Record_Model_Core extends ORM { + function item() { + return model_cache::get("item", $this->item_id); + } +} diff --git a/modules/search/module.info b/modules/search/module.info new file mode 100644 index 0000000..f1bb1fa --- /dev/null +++ b/modules/search/module.info @@ -0,0 +1,7 @@ +name = "Search" +description = "Allows users to search their Gallery" +version = 1 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:search" +discuss_url = "http://galleryproject.org/forum_module_search" diff --git a/modules/search/views/search.html.php b/modules/search/views/search.html.php new file mode 100644 index 0000000..a42c31d --- /dev/null +++ b/modules/search/views/search.html.php @@ -0,0 +1,65 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? // @todo Set hover on AlbumGrid list items ?> +<form action="<?= url::site("/search") ?>" id="g-search-form" class="g-short-form"> + <fieldset> + <legend> + <?= t("Search") ?> + </legend> + <ul> + <li> + <? if ($album->id == item::root()->id): ?> + <label for="q"><?= t("Search the gallery") ?></label> + <? else: ?> + <label for="q"><?= t("Search this album") ?></label> + <? endif; ?> + <input name="album" type="hidden" value="<?= html::clean_attribute($album->id) ?>" /> + <input name="q" id="q" type="text" value="<?= html::clean_attribute($q) ?>" class="text" /> + </li> + <li> + <input type="submit" value="<?= t("Search")->for_html_attr() ?>" class="submit" /> + </li> + </ul> + </fieldset> +</form> + +<div id="g-search-results"> + <h1><?= t("Search results") ?></h1> + + <? if ($album->id == item::root()->id): ?> + <div> + <?= t("Searched the whole gallery.") ?> + </div> + <? else: ?> + <div> + <?= t("Searched within album <b>%album</b>.", array("album" => html::purify($album->title))) ?> + <a href="<?= url::site(url::merge(array("album" => item::root()->id))) ?>"><?= t("Search whole gallery") ?></a> + </div> + <? endif; ?> + + <? if (count($items)): ?> + <ul id="g-album-grid" class="ui-helper-clearfix"> + <? foreach ($items as $item): ?> + <? $item_class = $item->is_album() ? "g-album" : "g-photo" ?> + <li class="g-item <?= $item_class ?>"> + <a href="<?= $item->url() ?>"> + <?= $item->thumb_img(array("class" => "g-thumbnail")) ?> + <p> + <span class="<?= $item_class ?>"></span> + <?= html::purify(text::limit_chars($item->title, 32, "…")) ?> + </p> + <div> + <?= nl2br(html::purify(text::limit_chars($item->description, 64, "…"))) ?> + </div> + </a> + </li> + <? endforeach ?> + </ul> + <?= $theme->paginator() ?> + + <? else: ?> + <p> + <?= t("No results found for <b>%term</b>", array("term" => $q)) ?> + </p> + + <? endif; ?> +</div> diff --git a/modules/search/views/search_link.html.php b/modules/search/views/search_link.html.php new file mode 100644 index 0000000..4f9abc1 --- /dev/null +++ b/modules/search/views/search_link.html.php @@ -0,0 +1,22 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<form action="<?= url::site("search") ?>" id="g-quick-search-form" class="g-short-form"> + <? if (isset($item)): ?> + <? $album_id = $item->is_album() ? $item->id : $item->parent_id; ?> + <? else: ?> + <? $album_id = item::root()->id; ?> + <? endif; ?> + <ul> + <li> + <? if ($album_id == item::root()->id): ?> + <label for="g-search"><?= t("Search the gallery") ?></label> + <? else: ?> + <label for="g-search"><?= t("Search this album") ?></label> + <? endif; ?> + <input type="hidden" name="album" value="<?= $album_id ?>" /> + <input type="text" name="q" id="g-search" class="text" /> + </li> + <li> + <input type="submit" value="<?= t("Go")->for_html_attr() ?>" class="submit" /> + </li> + </ul> +</form> diff --git a/modules/server_add/controllers/admin_server_add.php b/modules/server_add/controllers/admin_server_add.php new file mode 100644 index 0000000..ba2b9b3 --- /dev/null +++ b/modules/server_add/controllers/admin_server_add.php @@ -0,0 +1,97 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Server_Add_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Add from server"); + $view->content = new View("admin_server_add.html"); + $view->content->form = $this->_get_admin_form(); + $paths = unserialize(module::get_var("server_add", "authorized_paths", "a:0:{}")); + $view->content->paths = array_keys($paths); + + print $view; + } + + public function add_path() { + access::verify_csrf(); + + $form = $this->_get_admin_form(); + $paths = unserialize(module::get_var("server_add", "authorized_paths", "a:0:{}")); + if ($form->validate()) { + $path = html_entity_decode($form->add_path->path->value); + if (is_link($path)) { + $form->add_path->path->add_error("is_symlink", 1); + } else if (!is_readable($path)) { + $form->add_path->path->add_error("not_readable", 1); + } else { + $paths[$path] = 1; + module::set_var("server_add", "authorized_paths", serialize($paths)); + message::success(t("Added path %path", array("path" => $path))); + server_add::check_config($paths); + url::redirect("admin/server_add"); + } + } + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_server_add.html"); + $view->content->form = $form; + $view->content->paths = array_keys($paths); + print $view; + } + + public function remove_path() { + access::verify_csrf(); + + $path = Input::instance()->get("path"); + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + if (isset($paths[$path])) { + unset($paths[$path]); + message::success(t("Removed path %path", array("path" => $path))); + module::set_var("server_add", "authorized_paths", serialize($paths)); + server_add::check_config($paths); + } + url::redirect("admin/server_add"); + } + + public function autocomplete() { + $directories = array(); + + $path_prefix = Input::instance()->get("q"); + foreach (glob("{$path_prefix}*") as $file) { + if (is_dir($file) && !is_link($file)) { + $directories[] = html::clean($file); + } + } + + ajax::response(implode("\n", $directories)); + } + + private function _get_admin_form() { + $form = new Forge("admin/server_add/add_path", "", "post", + array("id" => "g-server-add-admin-form", "class" => "g-short-form")); + $add_path = $form->group("add_path"); + $add_path->input("path")->label(t("Path"))->rules("required")->id("g-path") + ->error_messages("not_readable", t("This directory is not readable by the webserver")) + ->error_messages("is_symlink", t("Symbolic links are not allowed")); + $add_path->submit("add")->value(t("Add Path")); + + return $form; + } +} diff --git a/modules/server_add/controllers/server_add.php b/modules/server_add/controllers/server_add.php new file mode 100644 index 0000000..f6e0a94 --- /dev/null +++ b/modules/server_add/controllers/server_add.php @@ -0,0 +1,315 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Server_Add_Controller extends Admin_Controller { + public function browse($id) { + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + foreach (array_keys($paths) as $path) { + $files[] = $path; + } + + // Clean leftover task rows. There really should be support for this in the task framework + db::build() + ->where("task_id", "NOT IN", db::build()->select("id")->from("tasks")) + ->delete("server_add_entries") + ->execute(); + + $item = ORM::factory("item", $id); + $view = new View("server_add_tree_dialog.html"); + $view->item = $item; + $view->tree = new View("server_add_tree.html"); + $view->tree->files = $files; + $view->tree->parents = array(); + print $view; + } + + public function children() { + $path = Input::instance()->get("path"); + + $tree = new View("server_add_tree.html"); + $tree->files = array(); + $tree->parents = array(); + + // Make a tree with the parents back up to the authorized path, and all the children under the + // current path. + if (server_add::is_valid_path($path)) { + $tree->parents[] = $path; + while (server_add::is_valid_path(dirname($tree->parents[0]))) { + array_unshift($tree->parents, dirname($tree->parents[0])); + } + + $glob_path = str_replace(array("{", "}", "[", "]"), array("\{", "\}", "\[", "\]"), $path); + foreach (glob("$glob_path/*") as $file) { + if (!is_readable($file)) { + continue; + } + if (!is_dir($file)) { + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (!legal_file::get_extensions($ext)) { + continue; + } + } + + $tree->files[] = $file; + } + } else { + // Missing or invalid path; print out the list of authorized path + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + foreach (array_keys($paths) as $path) { + $tree->files[] = $path; + } + } + print $tree; + } + + /** + * Begin the task of adding photos. + */ + public function start() { + access::verify_csrf(); + $item = ORM::factory("item", Input::instance()->get("item_id")); + + $task_def = Task_Definition::factory() + ->callback("Server_Add_Controller::add") + ->description(t("Add photos or movies from the local server")) + ->name(t("Add from server")); + $task = task::create($task_def, array("item_id" => $item->id)); + + foreach (Input::instance()->post("paths") as $path) { + if (server_add::is_valid_path($path)) { + $entry = ORM::factory("server_add_entry"); + $entry->path = $path; + $entry->is_directory = intval(is_dir($path)); + $entry->parent_id = null; + $entry->task_id = $task->id; + $entry->save(); + } + } + + json::reply( + array("result" => "started", + "status" => (string)$task->status, + "url" => url::site("server_add/run/$task->id?csrf=" . access::csrf_token()))); + } + + /** + * Run the task of adding photos + */ + function run($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded() || $task->owner_id != identity::active_user()->id) { + access::forbidden(); + } + + $task = task::run($task_id); + // Prevent the JavaScript code from breaking by forcing a period as + // decimal separator for all locales with sprintf("%F", $value). + json::reply(array("done" => (bool)$task->done, + "status" => (string)$task->status, + "percent_complete" => sprintf("%F", $task->percent_complete))); + } + + /** + * This is the task code that adds photos and albums. It first examines all the target files + * and creates a set of Server_Add_Entry_Models, then runs through the list of models and adds + * them one at a time. + */ + static function add($task) { + $mode = $task->get("mode", "init"); + $start = microtime(true); + + switch ($mode) { + case "init": + $task->set("mode", "build-file-list"); + $task->set("dirs_scanned", 0); + $task->percent_complete = 0; + $task->status = t("Starting up"); + batch::start(); + break; + + case "build-file-list": // 0% to 10% + // We can't fit an arbitrary number of paths in a task, so store them in a separate table. + // Don't use an iterator here because we can't get enough control over it when we're dealing + // with a deep hierarchy and we don't want to go over our time quota. + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + $dirs_scanned = $task->get("dirs_scanned"); + while (microtime(true) - $start < 0.5) { + // Process every directory that doesn't yet have a parent id, these are the + // paths that we're importing. + $entry = ORM::factory("server_add_entry") + ->where("task_id", "=", $task->id) + ->where("is_directory", "=", 1) + ->where("checked", "=", 0) + ->order_by("id", "ASC") + ->find(); + + if ($entry->loaded()) { + $child_paths = glob(preg_quote($entry->path) . "/*"); + if (!$child_paths) { + $child_paths = glob("{$entry->path}/*"); + } + foreach ($child_paths as $child_path) { + if (!is_dir($child_path)) { + $ext = strtolower(pathinfo($child_path, PATHINFO_EXTENSION)); + if (!legal_file::get_extensions($ext) || !filesize($child_path)) { + // Not importable, skip it. + continue; + } + } + + $child_entry = ORM::factory("server_add_entry"); + $child_entry->task_id = $task->id; + $child_entry->path = $child_path; + $child_entry->parent_id = $entry->id; // null if the parent was a staging dir + $child_entry->is_directory = is_dir($child_path); + $child_entry->save(); + } + + // We've processed this entry, mark it as done. + $entry->checked = 1; + $entry->save(); + $dirs_scanned++; + } + } + + // We have no idea how long this can take because we have no idea how deep the tree + // hierarchy rabbit hole goes. Leave ourselves room here for 100 iterations and don't go + // over 10% in percent_complete. + $task->set("dirs_scanned", $dirs_scanned); + $task->percent_complete = min($task->percent_complete + 0.1, 10); + $task->status = t2("Scanned one directory", "Scanned %count directories", $dirs_scanned); + + if (!$entry->loaded()) { + $task->set("mode", "add-files"); + $task->set( + "total_files", + ORM::factory("server_add_entry")->where("task_id", "=", $task->id)->count_all()); + $task->percent_complete = 10; + } + break; + + case "add-files": // 10% to 100% + $completed_files = $task->get("completed_files", 0); + $total_files = $task->get("total_files"); + + // Ordering by id ensures that we add them in the order that we created the entries, which + // will create albums first. Ignore entries which already have an Item_Model attached, + // they're done. + $entries = ORM::factory("server_add_entry") + ->where("task_id", "=", $task->id) + ->where("item_id", "IS", null) + ->order_by("id", "ASC") + ->limit(10) + ->find_all(); + if ($entries->count() == 0) { + // Out of entries, we're done. + $task->set("mode", "done"); + } + + $owner_id = identity::active_user()->id; + foreach ($entries as $entry) { + if (microtime(true) - $start > 0.5) { + break; + } + + // Look up the parent item for this entry. By now it should exist, but if none was + // specified, then this belongs as a child of the current item. + $parent_entry = ORM::factory("server_add_entry", $entry->parent_id); + if (!$parent_entry->loaded()) { + $parent = ORM::factory("item", $task->get("item_id")); + } else { + $parent = ORM::factory("item", $parent_entry->item_id); + } + + $name = basename($entry->path); + $title = item::convert_filename_to_title($name); + if ($entry->is_directory) { + $album = ORM::factory("item"); + $album->type = "album"; + $album->parent_id = $parent->id; + $album->name = $name; + $album->title = $title; + $album->owner_id = $owner_id; + $album->sort_order = $parent->sort_order; + $album->sort_column = $parent->sort_column; + $album->save(); + $entry->item_id = $album->id; + } else { + try { + $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + if (legal_file::get_photo_extensions($extension)) { + $photo = ORM::factory("item"); + $photo->type = "photo"; + $photo->parent_id = $parent->id; + $photo->set_data_file($entry->path); + $photo->name = $name; + $photo->title = $title; + $photo->owner_id = $owner_id; + $photo->save(); + $entry->item_id = $photo->id; + } else if (legal_file::get_movie_extensions($extension)) { + $movie = ORM::factory("item"); + $movie->type = "movie"; + $movie->parent_id = $parent->id; + $movie->set_data_file($entry->path); + $movie->name = $name; + $movie->title = $title; + $movie->owner_id = $owner_id; + $movie->save(); + $entry->item_id = $movie->id; + } else { + // This should never happen, because we don't add stuff to the list that we can't + // process. But just in, case.. set this to a non-null value so that we skip this + // entry. + $entry->item_id = 0; + $task->log("Skipping unknown file type: {$entry->path}"); + } + } catch (Exception $e) { + // This can happen if a photo file is invalid, like a BMP masquerading as a .jpg + $entry->item_id = 0; + $task->log("Skipping invalid file: {$entry->path}"); + } + } + + $completed_files++; + $entry->save(); + } + $task->set("completed_files", $completed_files); + $task->status = t("Adding photos / albums (%completed of %total)", + array("completed" => $completed_files, + "total" => $total_files)); + $task->percent_complete = $total_files ? 10 + 100 * ($completed_files / $total_files) : 100; + break; + + case "done": + batch::stop(); + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + ORM::factory("server_add_entry") + ->where("task_id", "=", $task->id) + ->delete_all(); + message::info(t2("Successfully added one photo / album", + "Successfully added %count photos / albums", + $task->get("completed_files"))); + } + } +} diff --git a/modules/server_add/css/server_add.css b/modules/server_add/css/server_add.css new file mode 100644 index 0000000..36746ab --- /dev/null +++ b/modules/server_add/css/server_add.css @@ -0,0 +1,38 @@ +#g-server-add button { + margin-bottom: .5em; +} + +#g-server-add-tree { + cursor: pointer; + padding-left: 4px; + width: 95%; +} + +#g-server-add-tree li { + padding: 0; + float: none; +} + +#g-server-add-tree span.selected { + background: #ddd; +} + +#g-server-add-tree { + border: 1px solid #ccc; + height: 20em; + overflow: auto; + margin-bottom: .5em; + padding: .5em; +} + +#g-server-add ul ul li { + padding-left: 1.2em; +} + +#g-server-add-paths li .ui-icon { + margin-top: .4em; +} + +#g-server-add-admin-form .textbox { + width: 400px; +} diff --git a/modules/server_add/helpers/server_add.php b/modules/server_add/helpers/server_add.php new file mode 100644 index 0000000..5cf08ce --- /dev/null +++ b/modules/server_add/helpers/server_add.php @@ -0,0 +1,49 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class server_add_Core { + static function check_config($paths=null) { + if ($paths === null) { + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + } + if (empty($paths)) { + site_status::warning( + t("Server Add needs configuration. <a href=\"%url\">Configure it now!</a>", + array("url" => html::mark_clean(url::site("admin/server_add")))), + "server_add_configuration"); + } else { + site_status::clear("server_add_configuration"); + } + } + + static function is_valid_path($path) { + if (!is_readable($path) || is_link($path)) { + return false; + } + + $authorized_paths = unserialize(module::get_var("server_add", "authorized_paths")); + foreach (array_keys($authorized_paths) as $valid_path) { + if (strpos($path, $valid_path) === 0) { + return true; + } + } + + return false; + } +} diff --git a/modules/server_add/helpers/server_add_event.php b/modules/server_add/helpers/server_add_event.php new file mode 100644 index 0000000..a718e2f --- /dev/null +++ b/modules/server_add/helpers/server_add_event.php @@ -0,0 +1,42 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class server_add_event_Core { + static function admin_menu($menu, $theme) { + $menu->get("settings_menu") + ->append(Menu::factory("link") + ->id("server_add") + ->label(t("Server add")) + ->url(url::site("admin/server_add"))); + } + + static function site_menu($menu, $theme) { + $item = $theme->item(); + $paths = unserialize(module::get_var("server_add", "authorized_paths")); + + if ($item && identity::active_user()->admin && $item->is_album() && !empty($paths) && + is_writable($item->is_album() ? $item->file_path() : $item->parent()->file_path())) { + $menu->get("add_menu") + ->append(Menu::factory("dialog") + ->id("server_add") + ->label(t("Server add")) + ->url(url::site("server_add/browse/$item->id"))); + } + } +} diff --git a/modules/server_add/helpers/server_add_installer.php b/modules/server_add/helpers/server_add_installer.php new file mode 100644 index 0000000..b62bbcf --- /dev/null +++ b/modules/server_add/helpers/server_add_installer.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-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class server_add_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE {server_add_entries} ( + `id` int(9) NOT NULL auto_increment, + `checked` boolean default 0, + `is_directory` boolean default 0, + `item_id` int(9), + `parent_id` int(9), + `path` varchar(255) NOT NULL, + `task_id` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + server_add::check_config(); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + $db->query("CREATE TABLE {server_add_files} ( + `id` int(9) NOT NULL auto_increment, + `task_id` int(9) NOT NULL, + `file` varchar(255) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("server_add", $version = 2); + } + + if ($version == 2) { + $db->query("ALTER TABLE {server_add_files} ADD COLUMN `item_id` int(9)"); + $db->query("ALTER TABLE {server_add_files} ADD COLUMN `parent_id` int(9)"); + module::set_version("server_add", $version = 3); + } + + if ($version == 3) { + $db->query("DROP TABLE {server_add_files}"); + $db->query("CREATE TABLE {server_add_entries} ( + `id` int(9) NOT NULL auto_increment, + `checked` boolean default 0, + `is_directory` boolean default 0, + `item_id` int(9), + `parent_id` int(9), + `path` varchar(255) NOT NULL, + `task_id` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("server_add", $version = 4); + } + } + + static function deactivate() { + site_status::clear("server_add_configuration"); + } +} diff --git a/modules/server_add/helpers/server_add_theme.php b/modules/server_add/helpers/server_add_theme.php new file mode 100644 index 0000000..7dfe658 --- /dev/null +++ b/modules/server_add/helpers/server_add_theme.php @@ -0,0 +1,27 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class server_add_theme_Core { + static function head($theme) { + if (identity::active_user()->admin) { + return $theme->css("server_add.css") + . $theme->script("server_add.js"); + } + } +}
\ No newline at end of file diff --git a/modules/server_add/js/server_add.js b/modules/server_add/js/server_add.js new file mode 100644 index 0000000..02dda4c --- /dev/null +++ b/modules/server_add/js/server_add.js @@ -0,0 +1,125 @@ +(function($) { + $.widget("ui.gallery_server_add", { + _init: function() { + var self = this; + $("#g-server-add-add-button", this.element).click(function(event) { + event.preventDefault(); + $(".g-progress-bar", this.element). + progressbar(). + progressbar("value", 0); + $("#g-server-add-progress", this.element).slideDown("fast", function() { self.start_add(); }); + }); + $("#g-server-add-pause-button", this.element).click(function(event) { + self.pause = true; + $("#g-server-add-pause-button", this.element).hide(); + $("#g-server-add-continue-button", this.element).show(); + }); + $("#g-server-add-continue-button", this.element).click(function(event) { + self.pause = false; + $("#g-server-add-pause-button", this.element).show(); + $("#g-server-add-continue-button", this.element).hide(); + self.run_add(); + }); + $("#g-server-add-close-button", this.element).click(function(event) { + $("#g-dialog").dialog("close"); + window.location.reload(); + }); + $("#g-server-add-tree span.g-directory", this.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-server-add-tree span.g-file, #g-server-add-tree span.g-directory", this.element).click(function(event) { + self.select_file(event); + }); + $("#g-server-add-tree span.g-directory", this.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-dialog").bind("dialogclose", function(event, ui) { + window.location.reload(); + }); + }, + + taskURL: null, + pause: false, + + start_add: function() { + var self = this; + var paths = []; + $.each($("span.selected", self.element), function () { + paths.push($(this).attr("ref")); + }); + + $("#g-server-add-add-button", this.element).hide(); + $("#g-server-add-pause-button", this.element).show(); + + $.ajax({ + url: START_URL, + type: "POST", + async: false, + data: { "paths[]": paths }, + dataType: "json", + success: function(data, textStatus) { + $("#g-status").html(data.status); + $(".g-progress-bar", self.element).progressbar("value", data.percent_complete); + self.taskURL = data.url; + setTimeout(function() { self.run_add(); }, 25); + } + }); + return false; + }, + + run_add: function () { + var self = this; + $.ajax({ + url: self.taskURL, + async: false, + dataType: "json", + success: function(data, textStatus) { + $("#g-status").html(data.status); + $(".g-progress-bar", self.element).progressbar("value", data.percent_complete); + if (data.done) { + $("#g-server-add-progress", this.element).slideUp(); + $("#g-server-add-add-button", this.element).show(); + $("#g-server-add-pause-button", this.element).hide(); + $("#g-server-add-continue-button", this.element).hide(); + } else { + if (!self.pause) { + setTimeout(function() { self.run_add(); }, 25); + } + } + } + }); + }, + + /** + * Load a new directory + */ + open_dir: function(event) { + var self = this; + var path = $(event.target).attr("ref"); + $.ajax({ + url: GET_CHILDREN_URL.replace("__PATH__", path), + success: function(data, textStatus) { + $("#g-server-add-tree", self.element).html(data); + $("#g-server-add-tree span.g-directory", self.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-server-add-tree span.g-file, #g-server-add-tree span.g-directory", this.element).click(function(event) { + self.select_file(event); + }); + } + }); + }, + + /** + * Manage file selection state. + */ + select_file: function (event) { + $(event.target).toggleClass("selected"); + if ($("#g-server-add span.selected").length) { + $("#g-server-add-add-button").enable(true).removeClass("ui-state-disabled"); + } else { + $("#g-server-add-add-button").enable(false).addClass("ui-state-disabled"); + } + } + }); +})(jQuery); diff --git a/modules/server_add/models/server_add_entry.php b/modules/server_add/models/server_add_entry.php new file mode 100644 index 0000000..572ebcb --- /dev/null +++ b/modules/server_add/models/server_add_entry.php @@ -0,0 +1,21 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Server_Add_Entry_Model_Core extends ORM { +} diff --git a/modules/server_add/module.info b/modules/server_add/module.info new file mode 100644 index 0000000..dc455c7 --- /dev/null +++ b/modules/server_add/module.info @@ -0,0 +1,7 @@ +name = "Server Add" +description = "Allows authorized users to load images directly from your web server" +version = 4 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:server_add" +discuss_url = "http://galleryproject.org/forum_module_server_add" diff --git a/modules/server_add/views/admin_server_add.html.php b/modules/server_add/views/admin_server_add.html.php new file mode 100644 index 0000000..f59e327 --- /dev/null +++ b/modules/server_add/views/admin_server_add.html.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<?= $theme->css("server_add.css") ?> +<?= $theme->css("jquery.autocomplete.css") ?> +<?= $theme->script("jquery.autocomplete.js") ?> +<script type="text/javascript"> +$("document").ready(function() { + $("#g-path").gallery_autocomplete( + "<?= url::site("__ARGS__") ?>".replace("__ARGS__", "admin/server_add/autocomplete"), + { + max: 256, + loadingClass: "g-loading-small", + }); +}); +</script> + +<div class="g-block"> + <h1> <?= t("Add from server administration") ?> </h1> + <div class="g-block-content"> + <?= $form ?> + <h2><?= t("Authorized paths") ?></h2> + <ul id="g-server-add-paths"> + <? if (empty($paths)): ?> + <li class="g-module-status g-info"><?= t("No authorized image source paths defined yet") ?></li> + <? endif ?> + + <? foreach ($paths as $id => $path): ?> + <li> + <?= html::clean($path) ?> + <a href="<?= url::site("admin/server_add/remove_path?path=" . urlencode($path) . "&csrf=" . access::csrf_token()) ?>" + id="icon_<?= $id ?>" + class="g-remove-dir g-button"> + <span class="ui-icon ui-icon-trash"> + <?= t("delete") ?> + </span> + </a> + </li> + <? endforeach ?> + </ul> + </div> +</div> diff --git a/modules/server_add/views/server_add_tree.html.php b/modules/server_add/views/server_add_tree.html.php new file mode 100644 index 0000000..9135432 --- /dev/null +++ b/modules/server_add/views/server_add_tree.html.php @@ -0,0 +1,37 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<li class="ui-icon-left"> + <span class="ui-icon ui-icon-folder-open"></span> + <span class="g-directory" ref=""> + <?= t("All") ?> + </span> + <ul> + + <? foreach ($parents as $dir): ?> + <li class="ui-icon-left"> + <span class="ui-icon ui-icon-folder-open"></span> + <span class="g-directory" ref="<?= html::clean_attribute($dir) ?>"> + <?= html::clean(basename($dir)) ?> + </span> + <ul> + <? endforeach ?> + + <? foreach ($files as $file): ?> + <li class="ui-icon-left"> + <span class="ui-icon <?= is_dir($file) ? "ui-icon-folder-collapsed" : "ui-icon-document" ?>"></span> + <span class="<?= is_dir($file) ? "g-directory" : "g-file" ?>" + ref="<?= html::clean_attribute($file) ?>" > + <?= html::clean(basename($file)) ?> + </span> + </li> + <? endforeach ?> + <? if (!$files): ?> + <li> <i> <?= t("empty") ?> </i> </li> + <? endif ?> + + <? foreach ($parents as $dir): ?> + </ul> + </li> + <? endforeach ?> + + </ul> +</li> diff --git a/modules/server_add/views/server_add_tree_dialog.html.php b/modules/server_add/views/server_add_tree_dialog.html.php new file mode 100644 index 0000000..824a86a --- /dev/null +++ b/modules/server_add/views/server_add_tree_dialog.html.php @@ -0,0 +1,52 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var GET_CHILDREN_URL = "<?= url::site("server_add/children?path=__PATH__") ?>"; + var START_URL = "<?= url::site("server_add/start?item_id={$item->id}&csrf=$csrf") ?>"; +</script> + +<div id="g-server-add"> + <h1 style="display: none;"><?= t("Add Photos to '%title'", array("title" => html::purify($item->title))) ?></h1> + + <p id="g-description"><?= t("Photos will be added to album:") ?></p> + <ul class="g-breadcrumbs"> + <? $i = 0 ?> + <? foreach ($item->parents() as $parent): ?> + <li<? if ($i == 0) print " class=\"g-first\"" ?>> <?= html::purify($parent->title) ?> </li> + <? $i++ ?> + <? endforeach ?> + <li class="g-active"> <?= html::purify($item->title) ?> </li> + </ul> + + <ul id="g-server-add-tree" class="g-checkbox-tree"> + <?= $tree ?> + </ul> + + <div id="g-server-add-progress" style="display: none"> + <div class="g-progress-bar"></div> + <div id="g-status"></div> + </div> + + <span> + <button id="g-server-add-add-button" class="ui-state-default ui-state-disabled ui-corner-all" + disabled="disabled"> + <?= t("Add") ?> + </button> + <button id="g-server-add-pause-button" class="ui-state-default ui-corner-all" style="display:none"> + <?= t("Pause") ?> + </button> + <button id="g-server-add-continue-button" class="ui-state-default ui-corner-all" style="display:none"> + <?= t("Continue") ?> + </button> + + <button id="g-server-add-close-button" class="ui-state-default ui-corner-all"> + <?= t("Close") ?> + </button> + </span> + + <script type="text/javascript"> + $("#g-server-add").ready(function() { + $("#g-server-add").gallery_server_add(); + }); + </script> + +</div> diff --git a/modules/slideshow/helpers/slideshow_event.php b/modules/slideshow/helpers/slideshow_event.php new file mode 100644 index 0000000..add1e33 --- /dev/null +++ b/modules/slideshow/helpers/slideshow_event.php @@ -0,0 +1,80 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class slideshow_event_Core { + static function pre_deactivate($data) { + if ($data->module == "rss") { + $data->messages["warn"][] = t("The Slideshow module requires the RSS module."); + } + } + + static function module_change($changes) { + if (!module::is_active("rss") || in_array("rss", $changes->deactivate)) { + site_status::warning( + t("The Slideshow module requires the RSS module. <a href=\"%url\">Activate the RSS module now</a>", + array("url" => html::mark_clean(url::site("admin/modules")))), + "slideshow_needs_rss"); + } else { + site_status::clear("slideshow_needs_rss"); + } + } + + static function album_menu($menu, $theme) { + $max_scale = module::get_var("slideshow", "max_scale"); + if ($theme->item()->descendants_count(array(array("type", "=", "photo")))) { + $menu->append(Menu::factory("link") + ->id("slideshow") + ->label(t("View slideshow")) + ->url("javascript:cooliris.embed.show(" . + "{maxScale:$max_scale,feed:'" . self::_feed_url($theme) . "'})") + ->css_id("g-slideshow-link")); + } + } + + static function photo_menu($menu, $theme) { + $max_scale = module::get_var("slideshow", "max_scale"); + $menu->append(Menu::factory("link") + ->id("slideshow") + ->label(t("View slideshow")) + ->url("javascript:cooliris.embed.show(" . + "{maxScale:$max_scale,feed:'" . self::_feed_url($theme) . "'})") + ->css_id("g-slideshow-link")); + } + + static function tag_menu($menu, $theme) { + $max_scale = module::get_var("slideshow", "max_scale"); + $menu->append(Menu::factory("link") + ->id("slideshow") + ->label(t("View slideshow")) + ->url("javascript:cooliris.embed.show(" . + "{maxScale:$max_scale,feed:'" . self::_feed_url($theme) . "'})") + ->css_id("g-slideshow-link")); + } + + private static function _feed_url($theme) { + if ($item = $theme->item()) { + if (!$item->is_album()) { + $item = $item->parent(); + } + return rss::url("gallery/album/{$item->id}"); + } else { + return rss::url("tag/tag/{$theme->tag()->id}"); + } + } +} diff --git a/modules/slideshow/helpers/slideshow_installer.php b/modules/slideshow/helpers/slideshow_installer.php new file mode 100644 index 0000000..22bd953 --- /dev/null +++ b/modules/slideshow/helpers/slideshow_installer.php @@ -0,0 +1,43 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class slideshow_installer { + static function install() { + module::set_var("slideshow", "max_scale", 0); + } + + static function upgrade($version) { + if ($version == 1) { + module::set_var("slideshow", "max_scale", 0); + module::set_version("slideshow", $version = 2); + } + } + + static function deactivate() { + site_status::clear("slideshow_needs_rss"); + } + + static function can_activate() { + $messages = array(); + if (!module::is_active("rss")) { + $messages["warn"][] = t("The Slideshow module requires the RSS module."); + } + return $messages; + } +} diff --git a/modules/slideshow/helpers/slideshow_theme.php b/modules/slideshow/helpers/slideshow_theme.php new file mode 100644 index 0000000..bb02212 --- /dev/null +++ b/modules/slideshow/helpers/slideshow_theme.php @@ -0,0 +1,26 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class slideshow_theme_Core { + static function page_bottom($theme) { + $proto = request::protocol(); + return "<script src=\"$proto://e.cooliris.com/slideshow/v/37732/go.js\" " . + "type=\"text/javascript\"></script>"; + } +} diff --git a/modules/slideshow/module.info b/modules/slideshow/module.info new file mode 100644 index 0000000..2d71f71 --- /dev/null +++ b/modules/slideshow/module.info @@ -0,0 +1,7 @@ +name = "Slideshow" +description = "Allows users to view a slideshow of photos" +version = 2 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:slideshow" +discuss_url = "http://galleryproject.org/forum_module_slideshow" diff --git a/modules/tag/controllers/admin_tags.php b/modules/tag/controllers/admin_tags.php new file mode 100644 index 0000000..19906c6 --- /dev/null +++ b/modules/tag/controllers/admin_tags.php @@ -0,0 +1,119 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Tags_Controller extends Admin_Controller { + public function index() { + $filter = Input::instance()->get("filter"); + + $view = new Admin_View("admin.html"); + $view->page_title = t("Manage tags"); + $view->content = new View("admin_tags.html"); + $view->content->filter = $filter; + + $query = ORM::factory("tag"); + if ($filter) { + $query->like("name", $filter); + } + $view->content->tags = $query->order_by("name", "ASC")->find_all(); + print $view; + } + + public function form_delete($id) { + $tag = ORM::factory("tag", $id); + if ($tag->loaded()) { + print tag::get_delete_form($tag); + } + } + + public function delete($id) { + access::verify_csrf(); + + $tag = ORM::factory("tag", $id); + if (!$tag->loaded()) { + throw new Kohana_404_Exception(); + } + + $form = tag::get_delete_form($tag); + if ($form->validate()) { + $name = $tag->name; + $tag->delete(); + message::success(t("Deleted tag %tag_name", array("tag_name" => $name))); + log::success("tags", t("Deleted tag %tag_name", array("tag_name" => $name))); + + json::reply(array("result" => "success", "location" => url::site("admin/tags"))); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_rename($id) { + $tag = ORM::factory("tag", $id); + if ($tag->loaded()) { + print InPlaceEdit::factory($tag->name) + ->action("admin/tags/rename/$id") + ->render(); + } + } + + public function rename($id) { + access::verify_csrf(); + + $tag = ORM::factory("tag", $id); + if (!$tag->loaded()) { + throw new Kohana_404_Exception(); + } + + $in_place_edit = InPlaceEdit::factory($tag->name) + ->action("admin/tags/rename/$tag->id") + ->rules(array("required", "length[1,64]")); + + if ($in_place_edit->validate()) { + $old_name = $tag->name; + $new_name_or_list = $in_place_edit->value(); + $tag_list = explode(",", $new_name_or_list); + + $tag->name = array_shift($tag_list); + $tag->save(); + + if (!empty($tag_list)) { + $this->_copy_items_for_tags($tag, $tag_list); + $message = t("Split tag <i>%old_name</i> into <i>%tag_list</i>", + array("old_name" => $old_name, "tag_list" => $new_name_or_list)); + } else { + $message = t("Renamed tag <i>%old_name</i> to <i>%new_name</i>", + array("old_name" => $old_name, "new_name" => $tag->name)); + } + + message::success($message); + log::success("tags", $message); + + json::reply(array("result" => "success", "location" => url::site("admin/tags"))); + } else { + json::reply(array("result" => "error", "form" => (string)$in_place_edit->render())); + } + } + + private function _copy_items_for_tags($tag, $tag_list) { + foreach ($tag->items() as $item) { + foreach ($tag_list as $new_tag_name) { + tag::add($item, trim($new_tag_name)); + } + } + } +} diff --git a/modules/tag/controllers/tag.php b/modules/tag/controllers/tag.php new file mode 100644 index 0000000..bada9ba --- /dev/null +++ b/modules/tag/controllers/tag.php @@ -0,0 +1,96 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Tag_Controller extends Controller { + public function __call($function, $args) { + $tag_id = $function; + $tag = ORM::factory("tag")->where("id", "=", $tag_id)->find(); + $page_size = module::get_var("gallery", "page_size", 9); + + $input = Input::instance(); + $show = $input->get("show"); + + if ($show) { + $child = ORM::factory("item", $show); + $index = tag::get_position($tag, $child); + if ($index) { + $page = ceil($index / $page_size); + $uri = "tag/$tag_id/" . urlencode($tag->name); + url::redirect($uri . ($page == 1 ? "" : "?page=$page")); + } + } else { + $page = (int) $input->get("page", "1"); + } + + $children_count = $tag->items_count(); + $offset = ($page-1) * $page_size; + $max_pages = max(ceil($children_count / $page_size), 1); + + // Make sure that the page references a valid offset + if ($page < 1) { + url::redirect(url::merge(array("page" => 1))); + } else if ($page > $max_pages) { + url::redirect(url::merge(array("page" => $max_pages))); + } + + $root = item::root(); + $template = new Theme_View("page.html", "collection", "tag"); + $template->set_global( + array("page" => $page, + "max_pages" => $max_pages, + "page_size" => $page_size, + "tag" => $tag, + "children" => $tag->items($page_size, $offset), + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance(t("Tag: %tag_name", array("tag_name" => $tag->name)), + $tag->url())->set_last()), + "children_count" => $children_count)); + $template->content = new View("dynamic.html"); + $template->content->title = t("Tag: %tag_name", array("tag_name" => $tag->name)); + print $template; + + item::set_display_context_callback("Tag_Controller::get_display_context", $tag->id); + } + + static function get_display_context($item, $tag_id) { + $tag = ORM::factory("tag", $tag_id); + $where = array(array("type", "!=", "album")); + + $position = tag::get_position($tag, $item, $where); + if ($position > 1) { + list ($previous_item, $ignore, $next_item) = $tag->items(3, $position - 2, $where); + } else { + $previous_item = null; + list ($next_item) = $tag->items(1, $position, $where); + } + + $root = item::root(); + return array("position" => $position, + "previous_item" => $previous_item, + "next_item" => $next_item, + "sibling_count" => $tag->items_count($where), + "siblings_callback" => array(array($tag, "items"), array()), + "breadcrumbs" => array( + Breadcrumb::instance($root->title, $root->url())->set_first(), + Breadcrumb::instance(t("Tag: %tag_name", array("tag_name" => $tag->name)), + $tag->url("show={$item->id}")), + Breadcrumb::instance($item->title, $item->url())->set_last())); + } +} diff --git a/modules/tag/controllers/tag_name.php b/modules/tag/controllers/tag_name.php new file mode 100644 index 0000000..f96ab69 --- /dev/null +++ b/modules/tag/controllers/tag_name.php @@ -0,0 +1,33 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Tag_Name_Controller extends Controller { + public function __call($function, $args) { + $tag_name = $function; + $tag = ORM::factory("tag")->where("name", "=", $tag_name)->find(); + if (!$tag->loaded()) { + // No matching tag was found. If this was an imported tag, this is probably a bug. + // If the user typed the URL manually, it might just be wrong + throw new Kohana_404_Exception(); + } + + url::redirect($tag->abs_url()); + } + +} diff --git a/modules/tag/controllers/tags.php b/modules/tag/controllers/tags.php new file mode 100644 index 0000000..77d45a9 --- /dev/null +++ b/modules/tag/controllers/tags.php @@ -0,0 +1,65 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Tags_Controller extends Controller { + public function index() { + // Far from perfection, but at least require view permission for the root album + $album = ORM::factory("item", 1); + access::required("view", $album); + + print tag::cloud(module::get_var("tag", "tag_cloud_size", 30)); + } + + public function create($item_id) { + $item = ORM::factory("item", $item_id); + access::required("view", $item); + access::required("edit", $item); + + $form = tag::get_add_form($item); + if ($form->validate()) { + 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); + } + } + + json::reply(array("result" => "success", "cloud" => (string)tag::cloud(30))); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function autocomplete() { + $tags = array(); + $tag_parts = explode(",", Input::instance()->get("q")); + $limit = Input::instance()->get("limit"); + $tag_part = ltrim(end($tag_parts)); + $tag_list = ORM::factory("tag") + ->where("name", "LIKE", Database::escape_for_like($tag_part) . "%") + ->order_by("name", "ASC") + ->limit($limit) + ->find_all(); + foreach ($tag_list as $tag) { + $tags[] = html::clean($tag->name); + } + + ajax::response(implode("\n", $tags)); + } +} diff --git a/modules/tag/css/tag.css b/modules/tag/css/tag.css new file mode 100644 index 0000000..8a64960 --- /dev/null +++ b/modules/tag/css/tag.css @@ -0,0 +1,102 @@ +/* Tag cloud ~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-tag-cloud ul { + font-size: 1.2em; + text-align: justify; +} + +#g-tag-cloud ul li { + display: inline; + line-height: 1.5em; + text-align: justify; +} + +#g-tag-cloud ul li a { + text-decoration: none; +} + +#g-tag-cloud ul li span { + display: none; +} + +#g-tag-cloud ul li.size0 a { + color: #9cf; + font-size: 70%; + font-weight: 100; +} + +#g-tag-cloud ul li.size1 a { + color: #9cf; + font-size: 80%; + font-weight: 100; +} + +#g-tag-cloud ul li.size2 a { + color: #69f; + font-size: 90%; + font-weight: 300; +} + +#g-tag-cloud ul li.size3 a { + color: #69c; + font-size: 100%; + font-weight: 500; +} + +#g-tag-cloud ul li.size4 a { + color: #369; + font-size: 110%; + font-weight: 700; +} + +#g-tag-cloud ul li.size5 a { + color: #0e2b52; + font-size: 120%; + font-weight: 900; +} + +#g-tag-cloud ul li.size6 a { + color: #0e2b52; + font-size: 130%; + font-weight: 900; +} + +#g-tag-cloud ul li.size7 a { + color: #0e2b52; + font-size: 140%; + font-weight: 900; +} + +#g-tag-cloud ul li a:hover { + color: #f30; + text-decoration: underline; +} + +/* Add tag form ~~~~~~~~~~~~~~~~~~~~ */ + +#g-sidebar .g-short-form .textbox { + width: 11em; +} + +/* Tag admin ~~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-tag-admin { + table-layout: fixed; +} + +#g-tag-admin td { + border: 0; + vertical-align: top; +} + +#g-tag-admin ul { + margin-bottom: 2em; +} + +#g-tag-admin li { + padding: .1em 0 .2em 0; +} + +#g-tag-admin form ul { + margin-bottom: 0; +} diff --git a/modules/tag/helpers/item_tags_rest.php b/modules/tag/helpers/item_tags_rest.php new file mode 100644 index 0000000..c153cb9 --- /dev/null +++ b/modules/tag/helpers/item_tags_rest.php @@ -0,0 +1,66 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class item_tags_rest_Core { + static function get($request) { + $item = rest::resolve($request->url); + $tags = array(); + foreach (tag::item_tags($item) as $tag) { + $tags[] = rest::url("tag_item", $tag, $item); + } + + return array( + "url" => $request->url, + "members" => $tags); + } + + static function post($request) { + $tag = rest::resolve($request->params->entity->tag); + $item = rest::resolve($request->params->entity->item); + access::required("view", $item); + + tag::add($item, $tag->name); + return array( + "url" => rest::url("tag_item", $tag, $item), + "members" => array( + rest::url("tag", $tag), + rest::url("item", $item))); + } + + static function delete($request) { + $item = rest::resolve($request->url); + access::required("edit", $item); + + // Deleting this collection means removing all tags associated with the item. + tag::clear_all($item); + } + + static function resolve($id) { + $item = ORM::factory("item", $id); + if (!access::can("view", $item)) { + throw new Kohana_404_Exception(); + } + + return $item; + } + + static function url($item) { + return url::abs_site("rest/item_tags/{$item->id}"); + } +} diff --git a/modules/tag/helpers/tag.php b/modules/tag/helpers/tag.php new file mode 100644 index 0000000..a946e01 --- /dev/null +++ b/modules/tag/helpers/tag.php @@ -0,0 +1,181 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_Core { + /** + * Associate a tag with an item. Create the tag if it doesn't already exist. + * + * @todo Write test. + * + * @param Item_Model $item an item + * @param string $tag_name a tag name + * @return Tag_Model + * @throws Exception("@todo {$tag_name} WAS_NOT_ADDED_TO {$item->id}") + */ + static function add($item, $tag_name) { + if (empty($tag_name)) { + throw new exception("@todo MISSING_TAG_NAME"); + } + + $tag = ORM::factory("tag")->where("name", "=", $tag_name)->find(); + if (!$tag->loaded()) { + $tag->name = $tag_name; + } + + $tag->add($item); + return $tag->save(); + } + + /** + * Return the N most popular tags. + * + * @return ORM_Iterator of Tag_Model in descending tag count order + */ + static function popular_tags($count) { + $count = max($count, 1); + return ORM::factory("tag") + ->order_by("count", "DESC") + ->limit($count) + ->find_all(); + } + + /** + * Return a rendering of the cloud for the N most popular tags. + * + * @param integer $count the number of tags + * @return View + */ + static function cloud($count) { + $tags = tag::popular_tags($count)->as_array(); + if ($tags) { + $cloud = new View("tag_cloud.html"); + $cloud->max_count = $tags[0]->count; + if (!$cloud->max_count) { + return; + } + usort($tags, array("tag", "sort_by_name")); + $cloud->tags = $tags; + return $cloud; + } + } + + static function sort_by_name($tag1, $tag2) { + return strcasecmp($tag1->name, $tag2->name); + } + + /** + * Return all the tags for a given item. + * @return array + */ + static function item_tags($item) { + return ORM::factory("tag") + ->join("items_tags", "tags.id", "items_tags.tag_id", "left") + ->where("items_tags.item_id", "=", $item->id) + ->find_all(); + } + + /** + * Return all the items for a given tag. + * @return array + */ + static function tag_items($tag) { + return ORM::factory("item") + ->join("items_tags", "items_tags.item_id", "items.id", "left") + ->where("items_tags.tag_id", "=", $tag->id) + ->find_all(); + } + + static function get_add_form($item) { + $form = new Forge("tags/create/{$item->id}", "", "post", array("id" => "g-add-tag-form", "class" => "g-short-form")); + $label = $item->is_album() ? + t("Add tag to album") : + ($item->is_photo() ? t("Add tag to photo") : t("Add tag to movie")); + + $group = $form->group("add_tag")->label("Add Tag"); + $group->input("name")->label($label)->rules("required")->id("name"); + $group->hidden("item_id")->value($item->id); + $group->submit("")->value(t("Add Tag")); + return $form; + } + + static function get_delete_form($tag) { + $form = new Forge("admin/tags/delete/$tag->id", "", "post", array("id" => "g-delete-tag-form")); + $group = $form->group("delete_tag") + ->label(t("Really delete tag %tag_name?", array("tag_name" => $tag->name))); + $group->submit("")->value(t("Delete Tag")); + return $form; + } + + /** + * Delete all tags associated with an item + */ + static function clear_all($item) { + db::build() + ->update("tags") + ->set("count", db::expr("`count` - 1")) + ->where("count", ">", 0) + ->where("id", "IN", db::build()->select("tag_id")->from("items_tags")->where("item_id", "=", $item->id)) + ->execute(); + db::build() + ->delete("items_tags") + ->where("item_id", "=", $item->id) + ->execute(); + } + + /** + * Remove all items from a tag + */ + static function remove_items($tag) { + db::build() + ->delete("items_tags") + ->where("tag_id", "=", $tag->id) + ->execute(); + $tag->count = 0; + $tag->save(); + } + + /** + * Get rid of any tags that have no associated items. + */ + static function compact() { + // @todo There's a potential race condition here which we can solve by adding a lock around + // this and all the cases where we create/update tags. I'm loathe to do that since it's an + // extremely rare case. + db::build()->delete("tags")->where("count", "=", 0)->execute(); + } + + /** + * Find the position of the given item in the tag collection. The resulting + * value is 1-indexed, so the first child in the album is at position 1. + * + * @param Tag_Model $tag + * @param Item_Model $item + * @param array $where an array of arrays, each compatible with ORM::where() + */ + static function get_position($tag, $item, $where=array()) { + return ORM::factory("item") + ->viewable() + ->join("items_tags", "items.id", "items_tags.item_id") + ->where("items_tags.tag_id", "=", $tag->id) + ->where("items.id", "<=", $item->id) + ->merge_where($where) + ->order_by("items.id") + ->count_all(); + } +}
\ No newline at end of file diff --git a/modules/tag/helpers/tag_block.php b/modules/tag/helpers/tag_block.php new file mode 100644 index 0000000..e7a7144 --- /dev/null +++ b/modules/tag/helpers/tag_block.php @@ -0,0 +1,45 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_block_Core { + static function get_site_list() { + return array("tag" => t("Popular tags")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "tag": + $block = new Block(); + $block->css_id = "g-tag"; + $block->title = t("Popular tags"); + $block->content = new View("tag_block.html"); + $block->content->cloud = tag::cloud(module::get_var("tag", "tag_cloud_size", 30)); + + if ($theme->item() && $theme->page_subtype() != "tag" && access::can("edit", $theme->item())) { + $controller = new Tags_Controller(); + $block->content->form = tag::get_add_form($theme->item()); + } else { + $block->content->form = ""; + } + break; + } + return $block; + } +}
\ No newline at end of file diff --git a/modules/tag/helpers/tag_event.php b/modules/tag/helpers/tag_event.php new file mode 100644 index 0000000..d62ae36 --- /dev/null +++ b/modules/tag/helpers/tag_event.php @@ -0,0 +1,165 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_event_Core { + /** + * Handle the creation of a new photo. + * @todo Get tags from the XMP and/or IPTC data in the image + * + * @param Item_Model $photo + */ + static function item_created($photo) { + $tags = array(); + if ($photo->is_photo()) { + $path = $photo->file_path(); + $size = getimagesize($photo->file_path(), $info); + if (is_array($info) && !empty($info["APP13"])) { + $iptc = iptcparse($info["APP13"]); + if (!empty($iptc["2#025"])) { + foreach($iptc["2#025"] as $tag) { + $tag = str_replace("\0", "", $tag); + foreach (explode(",", $tag) as $word) { + $word = trim($word); + $word = encoding::convert_to_utf8($word); + $tags[$word] = 1; + } + } + } + } + } + + // @todo figure out how to read the keywords from xmp + foreach(array_keys($tags) as $tag) { + try { + tag::add($photo, $tag); + } catch (Exception $e) { + Kohana_Log::add("error", "Error adding tag: $tag\n" . + $e->getMessage() . "\n" . $e->getTraceAsString()); + } + } + + return; + } + + static function item_deleted($item) { + tag::clear_all($item); + if (!batch::in_progress()) { + tag::compact(); + } + } + + static function batch_complete() { + tag::compact(); + } + + static function item_edit_form($item, $form) { + $url = url::site("tags/autocomplete"); + $form->script("") + ->text("$('form input[name=tags]').ready(function() { + $('form input[name=tags]').gallery_autocomplete( + '$url', {max: 30, multiple: true, multipleSeparator: ',', cacheLength: 1}); + });"); + + $tag_names = array(); + foreach (tag::item_tags($item) as $tag) { + $tag_names[] = $tag->name; + } + $form->edit_item->input("tags")->label(t("Tags (comma separated)")) + ->value(implode(", ", $tag_names)); + } + + static function item_edit_form_completed($item, $form) { + tag::clear_all($item); + foreach (explode(",", $form->edit_item->tags->value) as $tag_name) { + if ($tag_name) { + tag::add($item, trim($tag_name)); + } + } + module::event("item_related_update", $item); + tag::compact(); + } + + static function admin_menu($menu, $theme) { + $menu->get("content_menu") + ->append(Menu::factory("link") + ->id("tags") + ->label(t("Tags")) + ->url(url::site("admin/tags"))); + } + + static function item_index_data($item, $data) { + foreach (tag::item_tags($item) as $tag) { + $data[] = $tag->name; + } + } + + static function add_photos_form($album, $form) { + $group = $form->add_photos; + if (!is_object($group->uploadify)) { + return; + } + + $group->input("tags") + ->label(t("Add tags to all uploaded files")) + ->value(""); + $group->uploadify->script_data("tags", ""); + + $autocomplete_url = url::site("tags/autocomplete"); + $group->script("") + ->text("$('input[name=tags]') + .gallery_autocomplete( + '$autocomplete_url', + {max: 30, multiple: true, multipleSeparator: ',', cacheLength: 1} + ); + $('input[name=tags]') + .change(function (event) { + $('#g-uploadify').uploadifySettings('scriptData', {'tags': $(this).val()}); + });"); + } + + static function add_photos_form_completed($album, $form) { + $group = $form->add_photos; + if (!is_object($group->uploadify)) { + return; + } + + foreach (explode(",", $form->add_photos->tags->value) as $tag_name) { + $tag_name = trim($tag_name); + if ($tag_name) { + $tag = tag::add($album, $tag_name); + } + } + } + + static function info_block_get_metadata($block, $item) { + $tags = array(); + foreach (tag::item_tags($item) as $tag) { + $tags[] = "<a href=\"{$tag->url()}\">" . + html::clean($tag->name) . "</a>"; + } + if ($tags) { + $info = $block->content->metadata; + $info["tags"] = array( + "label" => t("Tags:"), + "value" => implode(", ", $tags) + ); + $block->content->metadata = $info; + } + } +} diff --git a/modules/tag/helpers/tag_installer.php b/modules/tag/helpers/tag_installer.php new file mode 100644 index 0000000..1fd18f3 --- /dev/null +++ b/modules/tag/helpers/tag_installer.php @@ -0,0 +1,59 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {tags} ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(128) NOT NULL, + `count` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE IF NOT EXISTS {items_tags} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9) NOT NULL, + `tag_id` int(9) NOT NULL, + PRIMARY KEY (`id`), + KEY(`tag_id`, `id`), + KEY(`item_id`, `id`)) + DEFAULT CHARSET=utf8;"); + module::set_var("tag", "tag_cloud_size", 30); + } + + static function upgrade($version) { + $db = Database::instance(); + if ($version == 1) { + $db->query("ALTER TABLE {tags} MODIFY COLUMN `name` VARCHAR(128)"); + module::set_version("tag", $version = 2); + } + if ($version == 2) { + module::set_var("tag", "tag_cloud_size", 30); + module::set_version("tag", $version = 3); + } + } + + static function uninstall() { + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {tags};"); + $db->query("DROP TABLE IF EXISTS {items_tags};"); + } +} diff --git a/modules/tag/helpers/tag_item_rest.php b/modules/tag/helpers/tag_item_rest.php new file mode 100644 index 0000000..0b7a77f --- /dev/null +++ b/modules/tag/helpers/tag_item_rest.php @@ -0,0 +1,51 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_item_rest_Core { + static function get($request) { + list ($tag, $item) = rest::resolve($request->url); + return array( + "url" => $request->url, + "entity" => array( + "tag" => rest::url("tag", $tag), + "item" => rest::url("item", $item))); + } + + static function delete($request) { + list ($tag, $item) = rest::resolve($request->url); + access::required("edit", $item); + $tag->remove($item); + $tag->save(); + } + + static function resolve($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)) { + throw new Kohana_404_Exception(); + } + + return array($tag, $item); + } + + static function url($tag, $item) { + return url::abs_site("rest/tag_item/{$tag->id},{$item->id}"); + } +} diff --git a/modules/tag/helpers/tag_items_rest.php b/modules/tag/helpers/tag_items_rest.php new file mode 100644 index 0000000..5d9919e --- /dev/null +++ b/modules/tag/helpers/tag_items_rest.php @@ -0,0 +1,64 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_items_rest_Core { + static function get($request) { + $tag = rest::resolve($request->url); + $items = array(); + foreach ($tag->items() as $item) { + if (access::can("view", $item)) { + $items[] = rest::url("tag_item", $tag, $item); + } + } + + return array( + "url" => $request->url, + "members" => $items); + } + + static function post($request) { + $tag = rest::resolve($request->params->entity->tag); + $item = rest::resolve($request->params->entity->item); + access::required("view", $item); + + if (!$tag->loaded()) { + throw new Kohana_404_Exception(); + } + + tag::add($item, $tag->name); + return array( + "url" => rest::url("tag_item", $tag, $item), + "members" => array( + "tag" => rest::url("tag", $tag), + "item" => rest::url("item", $item))); + } + + static function delete($request) { + $tag = rest::resolve($request->url); + $tag->remove_items(); + } + + static function resolve($id) { + return ORM::factory("tag", $id); + } + + static function url($tag) { + return url::abs_site("rest/tag_items/{$tag->id}"); + } +} diff --git a/modules/tag/helpers/tag_rest.php b/modules/tag/helpers/tag_rest.php new file mode 100644 index 0000000..9ccb67c --- /dev/null +++ b/modules/tag/helpers/tag_rest.php @@ -0,0 +1,88 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_rest_Core { + static function get($request) { + $tag = rest::resolve($request->url); + $tag_items = array(); + foreach ($tag->items() as $item) { + if (access::can("view", $item)) { + $tag_items[] = rest::url("tag_item", $tag, $item); + } + } + + return array( + "url" => $request->url, + "entity" => $tag->as_array(), + "relationships" => array( + "items" => array( + "url" => rest::url("tag_items", $tag), + "members" => $tag_items))); + } + + static function put($request) { + // Who can we allow to edit a tag name? If we allow anybody to do it then any logged in + // user can rename all your tags to something offensive. Right now limit renaming to admins. + if (!identity::active_user()->admin) { + access::forbidden(); + } + $tag = rest::resolve($request->url); + if (isset($request->params->entity->name)) { + $tag->name = $request->params->entity->name; + $tag->save(); + } + } + + static function delete($request) { + // Restrict deleting tags to admins. Otherwise, a logged in user can do great harm to an + // install. + if (!identity::active_user()->admin) { + access::forbidden(); + } + $tag = rest::resolve($request->url); + $tag->delete(); + } + + static function relationships($resource_type, $resource) { + switch ($resource_type) { + case "item": + $tags = array(); + foreach (tag::item_tags($resource) as $tag) { + $tags[] = rest::url("tag_item", $tag, $resource); + } + return array( + "tags" => array( + "url" => rest::url("item_tags", $resource), + "members" => $tags)); + } + } + + static function resolve($id) { + $tag = ORM::factory("tag", $id); + if (!$tag->loaded()) { + throw new Kohana_404_Exception(); + } + + return $tag; + } + + static function url($tag) { + return url::abs_site("rest/tag/{$tag->id}"); + } +} diff --git a/modules/tag/helpers/tag_rss.php b/modules/tag/helpers/tag_rss.php new file mode 100644 index 0000000..b432e23 --- /dev/null +++ b/modules/tag/helpers/tag_rss.php @@ -0,0 +1,48 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class tag_rss_Core { + static function available_feeds($item, $tag) { + if ($tag) { + $feeds["tag/tag/{$tag->id}"] = + t("Tag feed for %tag_name", array("tag_name" => $tag->name)); + return $feeds; + } + return array(); + } + + static function feed($feed_id, $offset, $limit, $id) { + if ($feed_id == "tag") { + $tag = ORM::factory("tag", $id); + if (!$tag->loaded()) { + throw new Kohana_404_Exception(); + } + + $feed = new stdClass(); + $feed->items = $tag->items($limit, $offset, "photo"); + $feed->max_pages = ceil($tag->count / $limit); + $feed->title = t("%site_title - %tag_name", + array("site_title" => item::root()->title, "tag_name" => $tag->name)); + $feed->description = t("Photos related to %tag_name", array("tag_name" => $tag->name)); + + return $feed; + } + } +} diff --git a/modules/tag/helpers/tag_task.php b/modules/tag/helpers/tag_task.php new file mode 100644 index 0000000..af9f2f1 --- /dev/null +++ b/modules/tag/helpers/tag_task.php @@ -0,0 +1,97 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_task_Core { + + static function available_tasks() { + $tasks[] = Task_Definition::factory() + ->callback("tag_task::clean_up_tags") + ->name(t("Clean up tags")) + ->description(t("Correct tag counts and remove tags with no items")) + ->severity(log::SUCCESS); + return $tasks; + } + + /** + * Fix up tag counts and delete any tags that have no associated items. + * @param Task_Model the task + */ + static function clean_up_tags($task) { + $errors = array(); + try { + $start = microtime(true); + $last_tag_id = $task->get("last_tag_id", null); + $current = 0; + $total = 0; + + switch ($task->get("mode", "init")) { + case "init": + $task->set("total", ORM::factory("tag")->count_all()); + $task->set("mode", "clean_up_tags"); + $task->set("completed", 0); + $task->set("last_tag_id", 0); + + case "clean_up_tags": + $completed = $task->get("completed"); + $total = $task->get("total"); + $last_tag_id = $task->get("last_tag_id"); + $tags = ORM::factory("tag")->where("id", ">", $last_tag_id)->find_all(25); + Kohana_Log::add("error",print_r(Database::instance()->last_query(),1)); + while ($current < $total && microtime(true) - $start < 1 && $tag = $tags->current()) { + $last_tag_id = $tag->id; + $real_count = $tag->items_count(); + if ($tag->count != $real_count) { + $tag->count = $real_count; + if ($tag->count) { + $task->log( + "Fixing count for tag {$tag->name} (id: {$tag->id}, new count: {$tag->count})"); + $tag->save(); + } else { + $task->log("Deleting empty tag {$tag->name} ({$tag->id})"); + $tag->delete(); + } + } + + $completed++; + $tags->next(); + } + $task->percent_complete = $completed / $total * 100; + $task->set("completed", $completed); + $task->set("last_tag_id", $last_tag_id); + } + + $task->status = t2("Examined %count tag", "Examined %count tags", $completed); + + if ($completed == $total) { + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + } + } catch (Exception $e) { + Kohana_Log::add("error",(string)$e); + $task->done = true; + $task->state = "error"; + $task->status = $e->getMessage(); + $errors[] = (string)$e; + } + if ($errors) { + $task->log($errors); + } + } +}
\ No newline at end of file diff --git a/modules/tag/helpers/tag_theme.php b/modules/tag/helpers/tag_theme.php new file mode 100644 index 0000000..81d1352 --- /dev/null +++ b/modules/tag/helpers/tag_theme.php @@ -0,0 +1,31 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tag_theme_Core { + static function head($theme) { + return $theme->css("jquery.autocomplete.css") + . $theme->script("jquery.autocomplete.js") + . $theme->css("tag.css"); + } + + static function admin_head($theme) { + return $theme->css("tag.css") + . $theme->script("gallery.in_place_edit.js"); + } +}
\ No newline at end of file diff --git a/modules/tag/helpers/tags_rest.php b/modules/tag/helpers/tags_rest.php new file mode 100644 index 0000000..88a349b --- /dev/null +++ b/modules/tag/helpers/tags_rest.php @@ -0,0 +1,77 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class tags_rest_Core { + /** + * Possible request parameters: + * start=# + * start at the Nth comment (zero based) + * + * num=# + * return up to N comments (max 100) + */ + static function get($request) { + $tags = array(); + + $num = 10; + $start = 0; + if (isset($request->params)) { + $p = $request->params; + $num = isset($p->num) ? min((int)$p->num, 100) : 10; + $start = isset($p->start) ? (int)$p->start : 0; + } + + foreach (ORM::factory("tag")->find_all($num, $start) as $tag) { + $tags[] = rest::url("tag", $tag); + } + return array("url" => rest::url("tags"), + "members" => $tags); + } + + static function post($request) { + // The user must have some edit permission somewhere to create a tag. + if (!identity::active_user()->admin) { + $query = db::build()->from("access_caches")->and_open(); + foreach (identity::active_user()->groups() as $group) { + $query->or_where("edit_{$group->id}", "=", access::ALLOW); + } + $has_any_edit_perm = $query->close()->count_records(); + if (!$has_any_edit_perm) { + access::forbidden(); + } + } + + if (empty($request->params->entity->name)) { + throw new Rest_Exception("Bad Request", 400); + } + + $tag = ORM::factory("tag")->where("name", "=", $request->params->entity->name)->find(); + if (!$tag->loaded()) { + $tag->name = $request->params->entity->name; + $tag->count = 0; + $tag->save(); + } + + return array("url" => rest::url("tag", $tag)); + } + + static function url() { + return url::abs_site("rest/tags"); + } +} diff --git a/modules/tag/models/tag.php b/modules/tag/models/tag.php new file mode 100644 index 0000000..0550910 --- /dev/null +++ b/modules/tag/models/tag.php @@ -0,0 +1,169 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Tag_Model_Core extends ORM { + protected $has_and_belongs_to_many = array("items"); + + public function __construct($id=null) { + parent::__construct($id); + + if (!$this->loaded()) { + // Set reasonable defaults + $this->count = 0; + } + } + + /** + * Return all viewable items associated with this tag. + * @param integer $limit number of rows to limit result to + * @param integer $offset offset in result to start returning rows from + * @param string $where an array of arrays, each compatible with ORM::where() + * @return ORM_Iterator + */ + public function items($limit=null, $offset=null, $where=array()) { + if (is_scalar($where)) { + // backwards compatibility + $where = array(array("items.type", "=", $where)); + } + return ORM::factory("item") + ->viewable() + ->join("items_tags", "items.id", "items_tags.item_id") + ->where("items_tags.tag_id", "=", $this->id) + ->merge_where($where) + ->order_by("items.id") + ->find_all($limit, $offset); + } + + /** + * Return the count of all viewable items associated with this tag. + * @param string $where an array of arrays, each compatible with ORM::where() + * @return integer + */ + public function items_count($where=array()) { + if (is_scalar($where)) { + // backwards compatibility + $where = array(array("items.type", "=", $where)); + } + return $model = ORM::factory("item") + ->viewable() + ->join("items_tags", "items.id", "items_tags.item_id") + ->where("items_tags.tag_id", "=", $this->id) + ->merge_where($where) + ->count_all(); + } + + /** + * Overload ORM::save() to trigger an item_related_update event for all items that are related + * to this tag. + */ + public function save() { + // Check to see if another tag exists with the same name + $duplicate_tag = ORM::factory("tag") + ->where("name", "=", $this->name) + ->where("id", "!=", $this->id) + ->find(); + if ($duplicate_tag->loaded()) { + // If so, tag its items with this tag so as to merge it + $duplicate_tag_items = ORM::factory("item") + ->join("items_tags", "items.id", "items_tags.item_id") + ->where("items_tags.tag_id", "=", $duplicate_tag->id) + ->find_all(); + foreach ($duplicate_tag_items as $item) { + $this->add($item); + } + + // ... and remove the duplicate tag + $duplicate_tag->delete(); + } + + if (isset($this->object_relations["items"])) { + $added = array_diff($this->changed_relations["items"], $this->object_relations["items"]); + $removed = array_diff($this->object_relations["items"], $this->changed_relations["items"]); + if (isset($this->changed_relations["items"])) { + $changed = array_merge($added, $removed); + } + $this->count = count($this->object_relations["items"]) + count($added) - count($removed); + } + + $result = parent::save(); + + if (!empty($changed)) { + foreach (ORM::factory("item")->where("id", "IN", $changed)->find_all() as $item) { + module::event("item_related_update", $item); + } + } + + return $result; + } + + /** + * Overload ORM::delete() to trigger an item_related_update event for all items that are + * related to this tag, and delete all items_tags relationships. + */ + public function delete($ignored_id=null) { + $related_item_ids = array(); + foreach (db::build() + ->select("item_id") + ->from("items_tags") + ->where("tag_id", "=", $this->id) + ->execute() as $row) { + $related_item_ids[$row->item_id] = 1; + } + + db::build()->delete("items_tags")->where("tag_id", "=", $this->id)->execute(); + $result = parent::delete(); + + if ($related_item_ids) { + foreach (ORM::factory("item") + ->where("id", "IN", array_keys($related_item_ids)) + ->find_all() as $item) { + module::event("item_related_update", $item); + } + } + return $result; + } + + /** + * Return the server-relative url to this item, eg: + * /gallery3/index.php/tags/35/Bob + * + * @param string $query the query string (eg "page=3") + */ + public function url($query=null) { + $url = url::site("tag/{$this->id}/" . urlencode($this->name)); + if ($query) { + $url .= "?$query"; + } + return $url; + } + + /** + * Return the full url to this item, eg: + * http://example.com/gallery3/index.php/tags/35/Bob + * + * @param string $query the query string (eg "page=3") + */ + public function abs_url($query=null) { + $url = url::abs_site("tag/{$this->id}/" . urlencode($this->name)); + if ($query) { + $url .= "?$query"; + } + return $url; + } +} diff --git a/modules/tag/module.info b/modules/tag/module.info new file mode 100644 index 0000000..19fbdb4 --- /dev/null +++ b/modules/tag/module.info @@ -0,0 +1,7 @@ +name = "Tags" +description = "Allows users to tag photos and albums" +version = 3 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:tag" +discuss_url = "http://galleryproject.org/forum_module_tag" diff --git a/modules/tag/views/admin_tags.html.php b/modules/tag/views/admin_tags.html.php new file mode 100644 index 0000000..e1db387 --- /dev/null +++ b/modules/tag/views/admin_tags.html.php @@ -0,0 +1,59 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $("document").ready(function() { + // using JS for adding link titles to avoid running t() for each tag + $("#g-tag-admin .g-tag-name").attr("title", <?= t("Click to edit this tag")->for_js() ?>); + $("#g-tag-admin .g-delete-link").attr("title", $(".g-delete-link:first span").html()); + + // In-place editing for tag admin + $(".g-editable").gallery_in_place_edit({ + form_url: <?= html::js_string(url::site("admin/tags/form_rename/__ID__")) ?> + }); + }); +</script> + +<? $tags_per_column = $tags->count()/5 ?> +<? $column_tag_count = 0 ?> + +<div class="g-block"> + <h1> <?= t("Manage tags") ?> </h1> + + <div class="g-block-content"> + <table id="g-tag-admin"> + <caption> + <?= t2("There is one tag", "There are %count tags", $tags->count()) ?> + </caption> + <tr> + <td> + <? foreach ($tags as $i => $tag): ?> + <? $current_letter = strtoupper(mb_substr($tag->name, 0, 1)) ?> + + <? if ($i == 0): /* first letter */ ?> + <strong><?= html::clean($current_letter) ?></strong> + <ul> + <? elseif ($last_letter != $current_letter): /* new letter */ ?> + </ul> + <? if ($column_tag_count > $tags_per_column): /* new column */ ?> + <? $column_tag_count = 0 ?> + </td> + <td> + <? endif ?> + <strong><?= html::clean($current_letter) ?></strong> + <ul> + <? endif ?> + <li> + <span class="g-editable g-tag-name" rel="<?= $tag->id ?>"><?= html::clean($tag->name) ?></span> + <span class="g-understate">(<?= $tag->count ?>)</span> + <a href="<?= url::site("admin/tags/form_delete/$tag->id") ?>" + class="g-dialog-link g-delete-link g-button"> + <span class="ui-icon ui-icon-trash"><?= t("Delete this tag") ?></span></a> + </li> + <? $column_tag_count++ ?> + <? $last_letter = $current_letter ?> + <? endforeach ?> + </ul> + </td> + </tr> + </table> + </div> +</div> diff --git a/modules/tag/views/tag_block.html.php b/modules/tag/views/tag_block.html.php new file mode 100644 index 0000000..d25b8dc --- /dev/null +++ b/modules/tag/views/tag_block.html.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + $("#g-add-tag-form").ready(function() { + var url = $("#g-tag-cloud-autocomplete-url").attr("href"); + $("#g-add-tag-form input:text").gallery_autocomplete( + url, { + max: 30, + multiple: true, + multipleSeparator: ',', + cacheLength: 1, + selectFirst: false + } + ); + $("#g-add-tag-form").ajaxForm({ + dataType: "json", + success: function(data) { + if (data.result == "success") { + $("#g-tag-cloud").html(data.cloud); + } + $("#g-add-tag-form").resetForm(); + } + }); + }); +</script> +<div id="g-tag-cloud"> + <a id="g-tag-cloud-autocomplete-url" style="display: none" + href="<?= url::site("tags/autocomplete") ?>"></a> + <?= $cloud ?> +</div> +<?= $form ?> diff --git a/modules/tag/views/tag_cloud.html.php b/modules/tag/views/tag_cloud.html.php new file mode 100644 index 0000000..c900ae7 --- /dev/null +++ b/modules/tag/views/tag_cloud.html.php @@ -0,0 +1,9 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<ul> + <? foreach ($tags as $tag): ?> + <li class="size<?=(int)(($tag->count / $max_count) * 7) ?>"> + <span><?= $tag->count ?> photos are tagged with </span> + <a href="<?= $tag->url() ?>" title="<?= $tag->count?> Bilder"><?= html::clean($tag->name) ?></a> + </li> + <? endforeach ?> +</ul> diff --git a/modules/thumbnav/changelog.log b/modules/thumbnav/changelog.log new file mode 100644 index 0000000..2efdcc2 --- /dev/null +++ b/modules/thumbnav/changelog.log @@ -0,0 +1,20 @@ +Thumb Navigator Changelog
+
+version 1.8
+- ADMIN: Added option to hide albums from the list
+- Module info adjusted to match new format in G3 3.0.2+
+- Fix: Added translation logic for help info
+- Fix: Added check for security access to the photo
+- Added support for modules which "hack" the item logic - instead of using access functionality, changed to use ->viewable()
+
+version 1.7:
+- Fix opacity settings for IE9
+
+version 1.6:
+- Some minor optimizations in Admin section
+
+version 1.5:
+- Some minor fixes
+
+version 1.4:
+- Fixed uninitialized state for content variable
\ No newline at end of file diff --git a/modules/thumbnav/controllers/admin_thumbnav.php b/modules/thumbnav/controllers/admin_thumbnav.php new file mode 100644 index 0000000..63b4b36 --- /dev/null +++ b/modules/thumbnav/controllers/admin_thumbnav.php @@ -0,0 +1,65 @@ +<?php defined("SYSPATH") or die("No direct script access."); + +class Admin_thumbnav_Controller extends Admin_Controller { + + public function index() { + $view = new Admin_View("admin.html"); + $view->content = new View("admin_thumbnav.html"); + $view->content->form = $this->_get_setting_form(); + $view->content->help = $this->get_edit_form_help(); + print $view; + } + + public function save() { + access::verify_csrf(); + + $form = $this->_get_setting_form(); + if ($form->validate()): + $thumb_count = $form->g_admin_thumbnavcfg->thumb_count->value; + if ($thumb_count==9): + module::clear_var("thumbnav", "thumb_count"); + else: + module::set_var("thumbnav", "thumb_count", $thumb_count); + endif; + if ($form->g_admin_thumbnavcfg->hidealbum->value): + module::set_var("thumbnav", "hide_albums", TRUE); + else: + module::clear_var("thumbnav", "hide_albums"); + endif; + + message::success("Settings have been Saved."); + url::redirect("admin/thumbnav"); + endif; + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_thumbnav.html"); + $view->content->form = $form; + $view->content->help = $this->get_edit_form_help(); + print $view; + } + + private function _get_setting_form() { + $form = new Forge("admin/thumbnav/save", "", "post", array("id" => "g-admin-thumbnav-form")); + $group = $form->group("g_admin_thumbnavcfg")->label(t("Settings")); + $group->input("thumb_count") + ->label(t("Thumbs displayed")) + ->rules("required|valid_digit") + ->value(module::get_var("thumbnav", "thumb_count", 9)); + $group->checkbox("hidealbum")->label(t("Hide Albums in the list")) + ->checked(module::get_var("thumbnav", "hide_albums")); + $form->submit("")->value(t("Save")); + return $form; + } + + protected function get_edit_form_help() { + $help = '<fieldset>'; + $help .= '<legend>Help</legend><ul>'; + $help .= '<li><h3>Settings</h3> + <p>Adjust Max Number of images in the block using <b>Thumbs displayed</b>. Default is 9. + </li>'; + + $help .= '</ul></fieldset>'; + return t($help); + } + +} diff --git a/modules/thumbnav/css/thumbnav.css b/modules/thumbnav/css/thumbnav.css new file mode 100644 index 0000000..549058e --- /dev/null +++ b/modules/thumbnav/css/thumbnav.css @@ -0,0 +1,11 @@ +/* Thumb Navigator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
+
+.g-thumbnav { text-align: center; padding: 0; }
+.g-navthumb { width: 62px; height: 62px; filter:alpha(opacity=55); opacity:.55; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=55)"; }
+
+.g-thumbnav ul { display: inline-block; padding: 0; margin: 0 0.5em; }
+.g-thumbnav li { float: left; border: transparent 1px solid;}
+.g-thumbnav li.g-current { border-color: #ddd; }
+
+.g-thumbnav li.g-current .g-navthumb,
+.g-navthumb:hover { background: #fff; filter:alpha(opacity=100); opacity:1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; }
diff --git a/modules/thumbnav/helpers/thumbnav_block.php b/modules/thumbnav/helpers/thumbnav_block.php new file mode 100644 index 0000000..898d691 --- /dev/null +++ b/modules/thumbnav/helpers/thumbnav_block.php @@ -0,0 +1,89 @@ +<?php defined("SYSPATH") or die("No direct script access."); + +class ThumbNav_block_Core { + + static function get_site_list() { + return array("thumbnav_block" => t("Navigator")); + } + + static function get($block_id, $theme) { + $block = ""; + switch ($block_id) { + case "thumbnav_block": + $item = $theme->item; + if ((!isset($item)) or (!$item->is_photo())): // Only should be used in photo pages + break; + endif; + + $hide_albums = module::get_var("thumbnav", "hide_albums", TRUE); + + $siblings = $item->parent()->children(); + $itemlist = Array(); + foreach ($siblings as $sibling): + if (isset($sibling)): + if ($sibling->viewable()): + if (($hide_albums and ($sibling->is_photo())) or (!$hide_albums)): + $itemlist[] = $sibling; + endif; + endif; + endif; + endforeach; + + $current = -1; + $total = count($itemlist); + + $thumb_count = module::get_var("thumbnav", "thumb_count", 9); + $thumb_count = min($thumb_count, $total); + + $shift_right = floor($thumb_count / 2); + $shift_left = $thumb_count - $shift_right - 1; + + for ($i = 1; $i <= $total; $i++): + if ($itemlist[$i-1]->rand_key == $item->rand_key): + $current = $i; + break; + endif; + endfor; + + $content = '<ul>'; + if ($current >= 1): + $first = $current - $shift_left; + $last = $current + $shift_right; + if ($first <= 0): + $last = min($last - $first + 1, $total); + $first = 1; + elseif ($last > $total): + $first = max($first - ($last - $total), 1); + $last = $total; + endif; + + for ($i = $first; $i <= $last; $i++): + $thumb_item = $itemlist[$i - 1]; + + if ($i == $current): + $content .= '<li class="g-current">'; + else: + $content .= '<li>'; + endif; + $content .= '<a href="' . $thumb_item->url() . '" title="' . html::purify($thumb_item->title) . '" target="_self">'; + $content .= $thumb_item->thumb_img(array("class" => "g-navthumb"), 60); + $content .= '</a></li>'; + endfor; + endif; + + $content .= "</ul>"; + $content .= "<div style=\"clear: both;\"></div>"; + + $block = new Block(); + $block->css_id = "g-thumbnav-block"; + $block->title = t("Navigator"); + $block->content = new View("thumbnav_block.html"); + $block->content->player = $content; + break; + } + + return $block; + } +} + +?>
\ No newline at end of file diff --git a/modules/thumbnav/helpers/thumbnav_event.php b/modules/thumbnav/helpers/thumbnav_event.php new file mode 100644 index 0000000..c0e8243 --- /dev/null +++ b/modules/thumbnav/helpers/thumbnav_event.php @@ -0,0 +1,20 @@ +<?php defined("SYSPATH") or die("No direct script access."); +class thumbnav_event_Core { + + static function activate() { + thumbnav::check_config(); + } + + static function deactivate() { + site_status::clear("thumbnav_config"); + } + + static function admin_menu($menu, $theme) { + $menu + ->get("settings_menu") + ->append(Menu::factory("link") + ->id("thumbnav") + ->label(t("Thumb Navigator")) + ->url(url::site("admin/thumbnav"))); + } +} diff --git a/modules/thumbnav/helpers/thumbnav_theme.php b/modules/thumbnav/helpers/thumbnav_theme.php new file mode 100644 index 0000000..6f11b30 --- /dev/null +++ b/modules/thumbnav/helpers/thumbnav_theme.php @@ -0,0 +1,8 @@ +<?php defined("SYSPATH") or die("No direct script access."); + +class ThumbNav_theme_Core { + + static function head($theme) { + $theme->css("thumbnav.css"); + } +}
\ No newline at end of file diff --git a/modules/thumbnav/module.info b/modules/thumbnav/module.info new file mode 100644 index 0000000..fcb03ce --- /dev/null +++ b/modules/thumbnav/module.info @@ -0,0 +1,7 @@ +name = "Thumb Navigator" +description = "Sidebar block: Adds thumb navigation option in Photo View.<br />Version 1.8 | By <a href=http://blog.dragonsoft.us>Serguei Dosyukov</a> | <a href=http://codex.gallery2.org/Gallery3:Modules:thumbnav>Visit plugin Site</a> | <a href=http://gallery.menalto.com/node/95116>Support</a> | <a href=thumbnav>Settings</a>" +version = 18 +author_name = "Serguei Dosyukov" +author_url = "http://blog.dragonsoft.us/gallery-3/" +info_url = "http://codex.gallery2.org/Gallery3:Modules:thumbnav" +discuss_url = "http://gallery.menalto.com/node/95116" diff --git a/modules/thumbnav/views/admin_include.html.php b/modules/thumbnav/views/admin_include.html.php new file mode 100644 index 0000000..1853596 --- /dev/null +++ b/modules/thumbnav/views/admin_include.html.php @@ -0,0 +1,95 @@ +<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Grey Dragon Theme - a custom theme for Gallery 3
+ * This theme was designed and built by Serguei Dosyukov, whose blog you will find at http://blog.dragonsoft.us
+ * Copyright (C) 2009-2011 Serguei Dosyukov
+ *
+ * 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.
+ */
+?>
+<style>
+#g-header { margin-bottom: 10px; }
+#gd-admin { position: relative; font-size: 0.9em; }
+#gd-admin legend { width: 100.5%; padding: 0.4em 0.8em; margin-left: -1.1em; background: url(/themes/greydragon/images/blue-grad.png) #d5e6f2 repeat-x left top; border: #dfdfdf 1px solid; border-top-left-radius: 0.4em; border-top-right-radius: 0.4em; }
+
+.g-admin-left { float: left; width: 53%; }
+.g-admin-right { float: left; width: 46%; margin-left: 1%; margin-top: 1em; }
+.g-admin-right h3 { border-bottom: #a2bdbf 1px solid; margin-top: 0.3em; margin-bottom: 0.3em; }
+
+#gd-admin-head { position: relative; height: auto; clear: both; display: block; overflow: auto; font-size: 11px; padding: 0.4em 0.8em; background-color: #b7c9d6; border: #a2bdbf 1px solid; }
+#gd-admin-title { float: left; color: #333v42; font-weight: bold; font-size: 1.6em; text-shadow: #deeefa 0 1px 0; }
+#gd-admin-hlinks ul { float: right; margin-top: 0.4em; font-size: 11px; }
+#gd-admin-hlinks li { list-style-type: none; float: left; color: #618299; display: inline; }
+#gd-admin-hlinks a { font-weight: bold; font-size: 13px; }
+
+#gd-admin form { border: none; }
+#gd-admin fieldset { border: #ccc 1px solid; border-radius: 0.4em; }
+#gd-admin input.g-error { padding-left: 30px; border: none; }
+#gd-admin input.g-success { background-color: transparent; }
+#gd-admin input.g-warning { background-color: transparent; border: none; }
+#gd-admin p.g-error { padding-left: 30px; border: none; margin-bottom: 0; background-image: none; }
+
+#g-content { padding: 0 1em; width: 97%; font-size: 1em; }
+#g-content form ul li input { display: inline; float: left; margin-right: 0.8em; }
+#g-content form ul li select { display: inline; float: left; margin-right: 0.8em; width: 50.6%; }
+#g-content form ul li input[type='text'] { width: 50%; }
+#g-content form ul li textarea { height: 6em; }
+#g-content form input[type="submit"] { border: #5b86ab 2px solid; padding: 0.3em; color: #fff; background: url(/themes/greydragon/images/button-grad-vs.png) #5580a6 repeat-x left top; }
+#g-content form input[type="submit"]:hover,
+input.ui-state-hover { background-image: url(/themes/greydragon/images/button-grad-active-vs.png); border-color: #2e5475; color: #eaf2fa !important; }
+#g-content form #vercheck, #g-content form #shadowbox, #g-content form #organizecheck { display: none; }
+</style>
+
+<script>
+ $(document).ready( function() {
+ $('form').submit( function() {
+ $('input[type=submit]', this).attr('disabled', 'disabled');
+ });
+ });
+</script>
+
+<?
+ if ($is_module):
+ $admin_info = new ArrayObject(parse_ini_file(MODPATH . $name . "/module.info"), ArrayObject::ARRAY_AS_PROPS);
+ $version = number_format($admin_info->version / 10, 1, '.', '');
+ else:
+ $admin_info = new ArrayObject(parse_ini_file(THEMEPATH . $name . "/theme.info"), ArrayObject::ARRAY_AS_PROPS);
+ $version = $admin_info->version;
+ endif;
+?>
+
+<div id="gd-admin" class="g-block">
+ <div id="gd-admin-head">
+ <div id="gd-admin-title"><?= t($admin_info->name) ?> - <?= $version ?></div>
+ <div id="gd-admin-hlinks">
+ <ul><li><a href="http://blog.dragonsoft.us/gallery-3/" target="_blank"><?= t("Home") ?></a> | </li>
+ <? if (isset($admin_info->discuss_url)): ?>
+ <li><a href="<?= $admin_info->discuss_url; ?>" target="_blank"><?= t("Support") ?></a> | </li>
+ <? endif; ?>
+ <? if (isset($admin_info->info_url)): ?>
+ <li><a href="<?= $admin_info->info_url; ?>" target="_blank"><?= t("Download") ?></a> | </li>
+ <? endif; ?>
+ <? if (isset($admin_info->vote)): ?>
+ <li><a href="<?= $admin_info->vote; ?>" target="_blank"><?= t("Vote") ?></a> | </li>
+ <? endif; ?>
+ <li><a href="http://twitter.com/greydragon_th" target="_blank" title="<?= t("Follow Us on Twitter") ?>"><?= t("Follow Us") ?></a> | </li>
+ <li><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9MWBSVJMWMJEU" target="_blank" ><?= t("Coffee Fund") ?></a> </li>
+ </ul>
+ </div>
+ </div>
+ <div class="g-block-content g-admin-left">
+ <?= $form ?>
+ </div>
+ <div class="g-admin-right">
+ <?= $help ?>
+ </div>
+</div>
diff --git a/modules/thumbnav/views/admin_thumbnav.html.php b/modules/thumbnav/views/admin_thumbnav.html.php new file mode 100644 index 0000000..b542705 --- /dev/null +++ b/modules/thumbnav/views/admin_thumbnav.html.php @@ -0,0 +1,10 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<? + $view = new View("admin_include.html"); + + $view->is_module = TRUE; + $view->name = "thumbnav"; + $view->form = $form; + $view->help = $help; + print $view; +?> diff --git a/modules/thumbnav/views/thumbnav_block.html.php b/modules/thumbnav/views/thumbnav_block.html.php new file mode 100644 index 0000000..d4c2c76 --- /dev/null +++ b/modules/thumbnav/views/thumbnav_block.html.php @@ -0,0 +1,5 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> + +<div class="g-thumbnav"> + <?= $player ?> +</div>
\ No newline at end of file diff --git a/modules/user/config/identity.php b/modules/user/config/identity.php new file mode 100644 index 0000000..f2fcebf --- /dev/null +++ b/modules/user/config/identity.php @@ -0,0 +1,37 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* + * @package Identity + * + * User settings, defined as arrays, or "groups". If no group name is + * used when loading the cache library, the group named "default" will be used. + * + * Each group can be used independently, and multiple groups can be used at once. + * + * Group Options: + * driver - User backend driver. Gallery comes with Gallery user driver. + * allow_updates - Flag to indicate that the back end allows updates. + * params - Driver parameters, specific to each driver. + */ +$config["user"] = array ( + "driver" => "gallery", + "allow_updates" => true, + "params" => array(), +); diff --git a/modules/user/controllers/admin_users.php b/modules/user/controllers/admin_users.php new file mode 100644 index 0000000..ed589a3 --- /dev/null +++ b/modules/user/controllers/admin_users.php @@ -0,0 +1,442 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Users_Controller extends Admin_Controller { + public function index() { + $view = new Admin_View("admin.html"); + $view->page_title = t("Users and groups"); + $view->page_type = "collection"; + $view->page_subtype = "admin_users"; + $view->content = new View("admin_users.html"); + + // @todo: add this as a config option + $page_size = module::get_var("user", "page_size", 10); + $page = Input::instance()->get("page", "1"); + $builder = db::build(); + $user_count = $builder->from("users")->count_records(); + + // Pagination info + $view->page = $page; + $view->page_size = $page_size; + $view->children_count = $user_count; + $view->max_pages = ceil($view->children_count / $view->page_size); + + $view->content->pager = new Pagination(); + $view->content->pager->initialize( + array("query_string" => "page", + "total_items" => $user_count, + "items_per_page" => $page_size, + "style" => "classic")); + + // Make sure that the page references a valid offset + if ($page < 1) { + url::redirect(url::merge(array("page" => 1))); + } else if ($page > $view->content->pager->total_pages) { + url::redirect(url::merge(array("page" => $view->content->pager->total_pages))); + } + + // Join our users against the items table so that we can get a count of their items + // in the same query. + $view->content->users = ORM::factory("user") + ->order_by("users.name", "ASC") + ->find_all($page_size, $view->content->pager->sql_offset); + $view->content->groups = ORM::factory("group")->order_by("name", "ASC")->find_all(); + + print $view; + } + + public function add_user() { + access::verify_csrf(); + + $form = $this->_get_user_add_form_admin(); + try { + $user = ORM::factory("user"); + $valid = $form->validate(); + $user->name = $form->add_user->inputs["name"]->value; + $user->full_name = $form->add_user->full_name->value; + $user->password = $form->add_user->password->value; + $user->email = $form->add_user->email->value; + $user->url = $form->add_user->url->value; + $user->locale = $form->add_user->locale->value; + $user->admin = $form->add_user->admin->checked; + $user->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->add_user->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $user->save(); + module::event("user_add_form_admin_completed", $user, $form); + message::success(t("Created user %user_name", array("user_name" => $user->name))); + json::reply(array("result" => "success")); + } else { + print json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function add_user_form() { + print $this->_get_user_add_form_admin(); + } + + public function delete_user($id) { + access::verify_csrf(); + + if ($id == identity::active_user()->id || $id == user::guest()->id) { + access::forbidden(); + } + + $user = user::lookup($id); + if (empty($user)) { + throw new Kohana_404_Exception(); + } + + $form = $this->_get_user_delete_form_admin($user); + if($form->validate()) { + $name = $user->name; + $user->delete(); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + + $message = t("Deleted user %user_name", array("user_name" => $name)); + log::success("user", $message); + message::success($message); + json::reply(array("result" => "success")); + } + + public function delete_user_form($id) { + $user = user::lookup($id); + if (empty($user)) { + throw new Kohana_404_Exception(); + } + $v = new View("admin_users_delete_user.html"); + $v->user = $user; + $v->form = $this->_get_user_delete_form_admin($user); + print $v; + } + + public function edit_user($id) { + access::verify_csrf(); + + $user = user::lookup($id); + if (empty($user)) { + throw new Kohana_404_Exception(); + } + + $form = $this->_get_user_edit_form_admin($user); + try { + $valid = $form->validate(); + $user->name = $form->edit_user->inputs["name"]->value; + $user->full_name = $form->edit_user->full_name->value; + if ($form->edit_user->password->value) { + $user->password = $form->edit_user->password->value; + } + $user->email = $form->edit_user->email->value; + $user->url = $form->edit_user->url->value; + $user->locale = $form->edit_user->locale->value; + if ($user->id != identity::active_user()->id) { + $user->admin = $form->edit_user->admin->checked; + } + + $user->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_user->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $user->save(); + module::event("user_edit_form_admin_completed", $user, $form); + message::success(t("Changed user %user_name", array("user_name" => $user->name))); + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + } + + public function edit_user_form($id) { + $user = user::lookup($id); + if (empty($user)) { + throw new Kohana_404_Exception(); + } + + print $this->_get_user_edit_form_admin($user); + } + + public function add_user_to_group($user_id, $group_id) { + access::verify_csrf(); + $group = group::lookup($group_id); + $user = user::lookup($user_id); + $group->add($user); + $group->save(); + } + + public function remove_user_from_group($user_id, $group_id) { + access::verify_csrf(); + $group = group::lookup($group_id); + $user = user::lookup($user_id); + $group->remove($user); + $group->save(); + } + + public function group($group_id) { + $view = new View("admin_users_group.html"); + $view->group = group::lookup($group_id); + print $view; + } + + public function add_group() { + access::verify_csrf(); + + $form = $this->_get_group_add_form_admin(); + try { + $valid = $form->validate(); + $group = ORM::factory("group"); + $group->name = $form->add_group->inputs["name"]->value; + $group->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->add_group->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $group->save(); + message::success( + t("Created group %group_name", array("group_name" => $group->name))); + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function add_group_form() { + print $this->_get_group_add_form_admin(); + } + + public function delete_group($id) { + access::verify_csrf(); + + $group = group::lookup($id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + $form = $this->_get_group_delete_form_admin($group); + if ($form->validate()) { + $name = $group->name; + $group->delete(); + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + + $message = t("Deleted group %group_name", array("group_name" => $name)); + log::success("group", $message); + message::success($message); + json::reply(array("result" => "success")); + } + + public function delete_group_form($id) { + $group = group::lookup($id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + print $this->_get_group_delete_form_admin($group); + } + + public function edit_group($id) { + access::verify_csrf(); + + $group = group::lookup($id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + $form = $this->_get_group_edit_form_admin($group); + try { + $valid = $form->validate(); + $group->name = $form->edit_group->inputs["name"]->value; + $group->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_group->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $group->save(); + message::success( + t("Changed group %group_name", array("group_name" => $group->name))); + json::reply(array("result" => "success")); + } else { + $group->reload(); + message::error( + t("Failed to change group %group_name", array("group_name" => $group->name))); + json::reply(array("result" => "error", "html" => (string) $form)); + } + } + + public function edit_group_form($id) { + $group = group::lookup($id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + print $this->_get_group_edit_form_admin($group); + } + + /* User Form Definitions */ + static function _get_user_edit_form_admin($user) { + $form = new Forge( + "admin/users/edit_user/$user->id", "", "post", array("id" => "g-edit-user-form")); + $group = $form->group("edit_user")->label(t("Edit user")); + $group->input("name")->label(t("Username"))->id("g-username")->value($user->name) + ->error_messages("required", t("A name is required")) + ->error_messages("conflict", t("There is already a user with that username")) + ->error_messages("length", t("This name is too long")); + $group->input("full_name")->label(t("Full name"))->id("g-fullname")->value($user->full_name) + ->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); + $group->input("email")->label(t("Email"))->id("g-email")->value($user->email) + ->error_messages("required", t("You must enter a valid email address")) + ->error_messages("length", t("This email address is too long")) + ->error_messages("email", t("You must enter a valid email address")); + $group->input("url")->label(t("URL"))->id("g-url")->value($user->url) + ->error_messages("url", t("You must enter a valid URL")); + 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; + } + + static function _get_user_add_form_admin() { + $form = new Forge("admin/users/add_user", "", "post", array("id" => "g-add-user-form")); + $group = $form->group("add_user")->label(t("Add user")); + $group->input("name")->label(t("Username"))->id("g-username") + ->error_messages("required", t("A name is required")) + ->error_messages("length", t("This name is too long")) + ->error_messages("conflict", t("There is already a user with that username")); + $group->input("full_name")->label(t("Full name"))->id("g-fullname") + ->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); + $group->input("email")->label(t("Email"))->id("g-email") + ->error_messages("required", t("You must enter a valid email address")) + ->error_messages("length", t("This email address is too long")) + ->error_messages("email", t("You must enter a valid email address")); + $group->input("url")->label(t("URL"))->id("g-url") + ->error_messages("url", t("You must enter a valid URL")); + self::_add_locale_dropdown($group); + $group->checkbox("admin")->label(t("Admin"))->id("g-admin"); + + module::event("user_add_form_admin", $user, $form); + $group->submit("")->value(t("Add user")); + return $form; + } + + private static function _add_locale_dropdown(&$form, $user=null) { + $locales = locales::installed(); + foreach ($locales as $locale => $display_name) { + $locales[$locale] = SafeString::of_safe_html($display_name); + } + + // Put "none" at the first position in the array + $locales = array_merge(array("" => t("« none »")), $locales); + $selected_locale = ($user && $user->locale) ? $user->locale : ""; + $form->dropdown("locale") + ->label(t("Language preference")) + ->options($locales) + ->selected($selected_locale); + } + + private function _get_user_delete_form_admin($user) { + $form = new Forge("admin/users/delete_user/$user->id", "", "post", + array("id" => "g-delete-user-form")); + $group = $form->group("delete_user")->label( + t("Delete user %name?", array("name" => $user->display_name()))); + $group->submit("")->value(t("Delete")); + return $form; + } + + /* Group Form Definitions */ + private function _get_group_edit_form_admin($group) { + $form = new Forge("admin/users/edit_group/$group->id", "", "post", array("id" => "g-edit-group-form")); + $form_group = $form->group("edit_group")->label(t("Edit group")); + $form_group->input("name")->label(t("Name"))->id("g-name")->value($group->name) + ->error_messages("required", t("A name is required")); + $form_group->inputs["name"]->error_messages("conflict", t("There is already a group with that name")) + ->error_messages("required", t("You must enter a group name")) + ->error_messages("length", + t("The group name must be less than %max_length characters", + array("max_length" => 255))); + $form_group->submit("")->value(t("Save")); + return $form; + } + + private function _get_group_add_form_admin() { + $form = new Forge("admin/users/add_group", "", "post", array("id" => "g-add-group-form")); + $form_group = $form->group("add_group")->label(t("Add group")); + $form_group->input("name")->label(t("Name"))->id("g-name"); + $form_group->inputs["name"]->error_messages("conflict", t("There is already a group with that name")) + ->error_messages("required", t("You must enter a group name")); + $form_group->submit("")->value(t("Add group")); + return $form; + } + + private function _get_group_delete_form_admin($group) { + $form = new Forge("admin/users/delete_group/$group->id", "", "post", + array("id" => "g-delete-group-form")); + $form_group = $form->group("delete_group")->label( + t("Are you sure you want to delete group %group_name?", array("group_name" => $group->name))); + $form_group->submit("")->value(t("Delete")); + return $form; + } +} diff --git a/modules/user/controllers/password.php b/modules/user/controllers/password.php new file mode 100644 index 0000000..ea14144 --- /dev/null +++ b/modules/user/controllers/password.php @@ -0,0 +1,141 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Password_Controller extends Controller { + const ALLOW_MAINTENANCE_MODE = true; + const ALLOW_PRIVATE_GALLERY = true; + + public function reset() { + $form = self::_reset_form(); + if (request::method() == "post") { + // @todo separate the post from get parts of this function + access::verify_csrf(); + // Basic validation (was some user name specified?) + if ($form->validate()) { + $this->_send_reset($form); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } else { + print $form; + } + } + + public function do_reset() { + if (request::method() == "post") { + $this->_change_password(); + } else { + $user = user::lookup_by_hash(Input::instance()->get("key")); + if (!empty($user)) { + print $this->_new_password_form($user->hash); + } else { + throw new Exception("@todo FORBIDDEN", 503); + } + } + } + + private function _send_reset($form) { + $user_name = $form->reset->inputs["name"]->value; + $user = user::lookup_by_name($user_name); + if ($user && !empty($user->email)) { + $user->hash = random::hash(); + $user->save(); + $message = new View("reset_password.html"); + $message->confirm_url = url::abs_site("password/do_reset?key=$user->hash"); + $message->user = $user; + + Sendmail::factory() + ->to($user->email) + ->subject(t("Password Reset Request")) + ->header("Mime-Version", "1.0") + ->header("Content-type", "text/html; charset=UTF-8") + ->message($message->render()) + ->send(); + + log::success( + "user", + t("Password reset email sent for user %name", array("name" => $user->name))); + } else if (!$user) { + // Don't include the username here until you're sure that it's XSS safe + log::warning( + "user", t("Password reset email requested for user %user_name, which does not exist.", + array("user_name" => $user_name))); + } else { + log::warning( + "user", t("Password reset failed for %user_name (has no email address on record).", + array("user_name" => $user->name))); + } + + // Always pretend that an email has been sent to avoid leaking + // information on what user names are actually real. + message::success(t("Password reset email sent")); + json::reply(array("result" => "success")); + } + + private static function _reset_form() { + $form = new Forge(url::current(true), "", "post", array("id" => "g-reset-form")); + $group = $form->group("reset")->label(t("Reset Password")); + $group->input("name")->label(t("Username"))->id("g-name")->class(null) + ->rules("required") + ->error_messages("required", t("You must enter a user name")); + $group->submit("")->value(t("Reset")); + + return $form; + } + + private function _new_password_form($hash=null) { + $template = new Theme_View("page.html", "other", "reset"); + + $form = new Forge("password/do_reset", "", "post", array("id" => "g-change-password-form")); + $group = $form->group("reset")->label(t("Change Password")); + $hidden = $group->hidden("hash"); + if (!empty($hash)) { + $hidden->value($hash); + } + $minimum_length = module::get_var("user", "minimum_password_length", 5); + $input_password = $group->password("password")->label(t("Password"))->id("g-password") + ->rules($minimum_length ? "required|length[$minimum_length, 40]" : "length[40]"); + $group->password("password2")->label(t("Confirm Password"))->id("g-password2") + ->matches($group->password); + $group->inputs["password2"]->error_messages( + "mistyped", t("The password and the confirm password must match")); + $group->submit("")->value(t("Update")); + + $template->content = $form; + return $template; + } + + private function _change_password() { + $view = $this->_new_password_form(); + if ($view->content->validate()) { + $user = user::lookup_by_hash(Input::instance()->post("hash")); + if (empty($user)) { + throw new Exception("@todo FORBIDDEN", 503); + } + + $user->password = $view->content->reset->password->value; + $user->hash = null; + $user->save(); + message::success(t("Password reset successfully")); + url::redirect(item::root()->abs_url()); + } else { + print $view; + } + } +}
\ No newline at end of file diff --git a/modules/user/controllers/users.php b/modules/user/controllers/users.php new file mode 100644 index 0000000..ee81344 --- /dev/null +++ b/modules/user/controllers/users.php @@ -0,0 +1,239 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Users_Controller extends Controller { + public function update($id) { + $user = user::lookup($id); + if (!$user || $user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + $form = $this->_get_edit_form($user); + try { + $valid = $form->validate(); + $user->full_name = $form->edit_user->full_name->value; + $user->url = $form->edit_user->url->value; + + if (count(locales::installed()) > 1 && + $user->locale != $form->edit_user->locale->value) { + $user->locale = $form->edit_user->locale->value; + $flush_locale_cookie = true; + } + + $user->validate(); + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_user->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + if (isset($flush_locale_cookie)) { + // Delete the session based locale preference + setcookie("g_locale", "", time() - 24 * 3600, "/"); + } + + $user->save(); + module::event("user_edit_form_completed", $user, $form); + message::success(t("User information updated")); + json::reply(array("result" => "success", + "resource" => url::site("users/{$user->id}"))); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function change_password($id) { + $user = user::lookup($id); + if (!$user || $user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + $form = $this->_get_change_password_form($user); + try { + $valid = $form->validate(); + $user->password = $form->change_password->password->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_password->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $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); + json::reply(array("result" => "success", + "resource" => url::site("users/{$user->id}"))); + } else { + log::warning("user", t("Failed password change for %name", array("name" => $user->name))); + $name = $user->name; + module::event("user_auth_failed", $name); + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function change_email($id) { + $user = user::lookup($id); + if (!$user || $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); + json::reply(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); + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_edit($id) { + $user = user::lookup($id); + if (!$user || $user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + print $this->_get_edit_form($user); + } + + public function form_change_password($id) { + $user = user::lookup($id); + if (!$user || $user->guest || $user->id != identity::active_user()->id) { + access::forbidden(); + } + + print $this->_get_change_password_form($user); + } + + public function form_change_email($id) { + $user = user::lookup($id); + if (!$user || $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_auth_attempts") + ->callback("user::valid_password") + ->error_messages("invalid_password", t("Incorrect password")) + ->error_messages( + "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")); + $group->script("") + ->text( + '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});'); + $group->password("password2")->label(t("Confirm new password"))->id("g-password2") + ->matches($group->password) + ->error_messages("matches", t("The passwords you entered do not match")); + + module::event("user_change_password_form", $user, $form); + $group->submit("")->value(t("Save")); + 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_password", 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_email_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("url")->label(t("URL"))->id("g-url")->value($user->url) + ->error_messages("url", t("You must enter a valid url")); + + module::event("user_edit_form", $user, $form); + $group->submit("")->value(t("Save")); + return $form; + } + + /** @todo combine with Admin_Users_Controller::_add_locale_dropdown */ + private function _add_locale_dropdown(&$form, $user=null) { + $locales = locales::installed(); + if (count($locales) <= 1) { + return; + } + + foreach ($locales as $locale => $display_name) { + $locales[$locale] = SafeString::of_safe_html($display_name); + } + + // Put "none" at the first position in the array + $locales = array_merge(array("" => t("« none »")), $locales); + $selected_locale = ($user && $user->locale) ? $user->locale : ""; + $form->dropdown("locale") + ->label(t("Language preference")) + ->options($locales) + ->selected($selected_locale); + } +} diff --git a/modules/user/css/user.css b/modules/user/css/user.css new file mode 100644 index 0000000..93e3d02 --- /dev/null +++ b/modules/user/css/user.css @@ -0,0 +1,120 @@ +/* User- and group-related form width ~~~~ */ + +#g-login-form, +#g-add-user-form +#g-edit-user-form, +#g-delete-user-form, +#g-user-admin { + width: 270px; +} + +/* User/group admin ~~~~~~~~~~~~~~~~~~~~~~ */ + +#g-user-admin { + width: auto; + margin-bottom: 4em; +} + +#g-group-admin { +} + +#g-user-admin-list .g-admin { + color: #55f; + font-weight: bold; +} + +.g-group { + display: block; + border: 1px solid #999; + margin: 0 1em 1em 0; + padding: 0; + width: 200px; +} + +.g-group h4 { + background-color: #eee; + border-bottom: 1px dashed #ccc; + padding: .5em 0 .5em .5em; +} + +.g-group .g-button { + padding: 0; +} + +.g-group .g-member-list, +.g-group div { + height: 180px; + margin: 1px; + overflow: auto; +} + +.g-group p { + margin-top: 1em; + padding: .5em; + text-align: center; +} + +.g-group .g-user { + padding: .2em 0 0 .5em; +} + +.g-group .g-user .g-button { + vertical-align: middle; +} + +.g-default-group h4, +.g-default-group .g-user { + color: #999; +} + +.g-group.ui-droppable { + padding: 0 !important; +} + +/* Password strength meter ~~~~~~~~~~~~~~~ */ + +.g-password-strength0 { + background: url(../images/progressImg1.png) no-repeat 0 0; + width: 138px; + height: 7px; +} + +.g-password-strength10 { + background-position:0 -7px; +} + +.g-password-strength20 { + background-position:0 -14px; +} + +.g-password-strength30 { + background-position:0 -21px; +} + +.g-password-strength40 { + background-position:0 -28px; +} + +.g-password-strength50 { + background-position:0 -35px; +} + +.g-password-strength60 { + background-position:0 -42px; +} + +.g-password-strength70 { + background-position:0 -49px; +} + +.g-password-strength80 { + background-position:0 -56px; +} + +.g-password-strength90 { + background-position:0 -63px; +} + +.g-password-strength100 { + background-position:0 -70px; +} diff --git a/modules/user/helpers/group.php b/modules/user/helpers/group.php new file mode 100644 index 0000000..59c9859 --- /dev/null +++ b/modules/user/helpers/group.php @@ -0,0 +1,82 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling groups. + * + * Note: by design, this class does not do any permission checking. + */ +class group_Core { + /** + * The group of all possible visitors. This includes the guest user. + * + * @return Group_Definition the group object + */ + static function everybody() { + return model_cache::get("group", 1); + } + + /** + * The group of all logged-in visitors. This does not include guest users. + * + * @return Group_Definition the group object + */ + static function registered_users() { + return model_cache::get("group", 2); + } + + /** + * Look up a group by id. + * @param integer $id the user id + * @return Group_Definition the group object, or null if the id was invalid. + */ + static function lookup($id) { + return self::_lookup_by_field("id", $id); + } + + /** + * Look up a group by name. + * @param integer $id the group name + * @return Group_Definition the group object, or null if the name was invalid. + */ + static function lookup_by_name($name) { + return self::_lookup_by_field("name", $name); + } + + /** + * Search the groups by the field and value. + * @param string $field_name column to look up the user by + * @param string $value value to match + * @return Group_Definition the group object, or null if the name was invalid. + */ + private static function _lookup_by_field($field_name, $value) { + try { + $group = model_cache::get("group", $value, $field_name); + if ($group->loaded()) { + return $group; + } + } catch (Exception $e) { + if (strpos($e->getMessage(), "MISSING_MODEL") === false) { + throw $e; + } + } + return null; + } +} diff --git a/modules/user/helpers/user.php b/modules/user/helpers/user.php new file mode 100644 index 0000000..f59cf76 --- /dev/null +++ b/modules/user/helpers/user.php @@ -0,0 +1,176 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This is the API for handling users. + * + * Note: by design, this class does not do any permission checking. + */ +class user_Core { + /** + * Return the guest user. + * + * @todo consider caching + * + * @return User_Model + */ + static function guest() { + return model_cache::get("user", 1); + } + + /** + * Return an admin user. Prefer the currently logged in user, if possible. + * + * @return User_Model + */ + static function admin_user() { + $active = identity::active_user(); + if ($active->admin) { + return $active; + } + + return ORM::factory("user")->where("admin", "=", 1)->order_by("id", "ASC")->find(); + } + + /** + * Is the password provided correct? + * + * @param user User Model + * @param string $password a plaintext password + * @return boolean true if the password is correct + */ + static function is_correct_password($user, $password) { + $valid = $user->password; + + // Try phpass first, since that's what we generate. + if (strlen($valid) == 34) { + require_once(MODPATH . "user/lib/PasswordHash.php"); + $hashGenerator = new PasswordHash(10, true); + return $hashGenerator->CheckPassword($password, $valid); + } + + $salt = substr($valid, 0, 4); + // Support both old (G1 thru 1.4.0; G2 thru alpha-4) and new password schemes: + $guess = (strlen($valid) == 32) ? md5($password) : ($salt . md5($salt . $password)); + if (!strcmp($guess, $valid)) { + return true; + } + + // Passwords with <&"> created by G2 prior to 2.1 were hashed with entities + $sanitizedPassword = html::chars($password, false); + $guess = (strlen($valid) == 32) ? md5($sanitizedPassword) + : ($salt . md5($salt . $sanitizedPassword)); + if (!strcmp($guess, $valid)) { + return true; + } + + return false; + } + + static function valid_password($password_input) { + if (!user::is_correct_password(identity::active_user(), $password_input->value)) { + $password_input->add_error("invalid_password", 1); + } + } + + static function valid_username($text_input) { + if (!self::lookup_by_name($text_input->value)) { + $text_input->add_error("invalid_username", 1); + } + } + + /** + * Create the hashed passwords. + * @param string $password a plaintext password + * @return string hashed password + */ + static function hash_password($password) { + require_once(MODPATH . "user/lib/PasswordHash.php"); + $hashGenerator = new PasswordHash(10, true); + return $hashGenerator->HashPassword($password); + } + + /** + * Look up a user by id. + * @param integer $id the user id + * @return User_Model the user object, or null if the id was invalid. + */ + static function lookup($id) { + return self::_lookup_user_by_field("id", $id); + } + + /** + * Look up a user by name. + * @param integer $name the user name + * @return User_Model the user object, or null if the name was invalid. + */ + static function lookup_by_name($name) { + return self::_lookup_user_by_field("name", $name); + } + + /** + * Look up a user by hash. + * @param integer $hash the user hash value + * @return User_Model the user object, or null if the name was invalid. + */ + static function lookup_by_hash($hash) { + return self::_lookup_user_by_field("hash", $hash); + } + + /** + * List the users + * @param mixed filters (@see Database.php + * @return array the user list. + */ + static function get_user_list($filter=array()) { + $user = ORM::factory("user"); + + foreach($filter as $method => $args) { + switch ($method) { + case "in": + $user->in($args[0], $args[1]); + break; + default: + $user->$method($args); + } + } + return $user->find_all(); + } + + /** + * Look up a user by field value. + * @param string search field + * @param string search value + * @return User_Core the user object, or null if the name was invalid. + */ + private static function _lookup_user_by_field($field_name, $value) { + try { + $user = model_cache::get("user", $value, $field_name); + if ($user->loaded()) { + return $user; + } + } catch (Exception $e) { + if (strpos($e->getMessage(), "MISSING_MODEL") === false) { + throw $e; + } + } + return null; + } +}
\ No newline at end of file diff --git a/modules/user/helpers/user_event.php b/modules/user/helpers/user_event.php new file mode 100644 index 0000000..40f6dde --- /dev/null +++ b/modules/user/helpers/user_event.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ + +class user_event_Core { + static function admin_menu($menu, $theme) { + $menu->add_after("appearance_menu", Menu::factory("link") + ->id("users_groups") + ->label(t("Users/Groups")) + ->url(url::site("admin/users"))); + + return $menu; + } +} diff --git a/modules/user/helpers/user_installer.php b/modules/user/helpers/user_installer.php new file mode 100644 index 0000000..67f6a3d --- /dev/null +++ b/modules/user/helpers/user_installer.php @@ -0,0 +1,142 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class user_installer { + static function can_activate() { + return array("warn" => array(IdentityProvider::confirmation_message())); + } + + static function activate() { + IdentityProvider::change_provider("user"); + // Set the latest version in initialize() below + } + + static function upgrade($version) { + if ($version == 1) { + module::set_var("user", "mininum_password_length", 5); + module::set_version("user", $version = 2); + } + + if ($version == 2) { + db::build() + ->update("users") + ->set("email", "unknown@unknown.com") + ->where("guest", "=", 0) + ->and_open() + ->where("email", "IS", null) + ->or_where("email", "=", "") + ->close() + ->execute(); + module::set_version("user", $version = 3); + } + + if ($version == 3) { + $password_length = module::get_var("user", "mininum_password_length", 5); + module::set_var("user", "minimum_password_length", $password_length); + module::clear_var("user", "mininum_password_length"); + module::set_version("user", $version = 4); + } + } + + static function uninstall() { + // Delete all users and groups so that we give other modules an opportunity to clean up + foreach (ORM::factory("user")->find_all() as $user) { + $user->delete(); + } + + foreach (ORM::factory("group")->find_all() as $group) { + $group->delete(); + } + + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {users};"); + $db->query("DROP TABLE IF EXISTS {groups};"); + $db->query("DROP TABLE IF EXISTS {groups_users};"); + } + + static function initialize() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {users} ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(32) NOT NULL, + `full_name` varchar(255) NOT NULL, + `password` varchar(64) NOT NULL, + `login_count` int(10) unsigned NOT NULL DEFAULT 0, + `last_login` int(10) unsigned NOT NULL DEFAULT 0, + `email` varchar(64) default NULL, + `admin` BOOLEAN default 0, + `guest` BOOLEAN default 0, + `hash` char(32) default NULL, + `url` varchar(255) default NULL, + `locale` char(10) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`hash`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE IF NOT EXISTS {groups} ( + `id` int(9) NOT NULL auto_increment, + `name` char(64) default NULL, + `special` BOOLEAN default 0, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE IF NOT EXISTS {groups_users} ( + `group_id` int(9) NOT NULL, + `user_id` int(9) NOT NULL, + PRIMARY KEY (`group_id`, `user_id`), + UNIQUE KEY(`user_id`, `group_id`)) + DEFAULT CHARSET=utf8;"); + + $everybody = ORM::factory("group"); + $everybody->name = "Everybody"; + $everybody->special = true; + $everybody->save(); + + $registered = ORM::factory("group"); + $registered->name = "Registered Users"; + $registered->special = true; + $registered->save(); + + $guest = ORM::factory("user"); + $guest->name = "guest"; + $guest->full_name = "Guest User"; + $guest->password = ""; + $guest->guest = true; + $guest->save(); + + $admin = ORM::factory("user"); + $admin->name = "admin"; + $admin->full_name = "Gallery Administrator"; + $admin->password = "admin"; + $admin->email = "unknown@unknown.com"; + $admin->admin = true; + $admin->save(); + + $root = ORM::factory("item", 1); + access::allow($everybody, "view", $root); + access::allow($everybody, "view_full", $root); + + access::allow($registered, "view", $root); + access::allow($registered, "view_full", $root); + + module::set_var("user", "minimum_password_length", 5); + } +}
\ No newline at end of file diff --git a/modules/user/helpers/user_theme.php b/modules/user/helpers/user_theme.php new file mode 100644 index 0000000..c608cdf --- /dev/null +++ b/modules/user/helpers/user_theme.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class user_theme_Core { + static function head($theme) { + return $theme->css("user.css") + . $theme->script("password_strength.js"); + } + + static function admin_head($theme) { + return $theme->css("user.css") + . $theme->script("password_strength.js"); + } +}
\ No newline at end of file diff --git a/modules/user/images/progressImg1.png b/modules/user/images/progressImg1.png Binary files differnew file mode 100644 index 0000000..a909364 --- /dev/null +++ b/modules/user/images/progressImg1.png diff --git a/modules/user/js/password_strength.js b/modules/user/js/password_strength.js new file mode 100644 index 0000000..2442b8d --- /dev/null +++ b/modules/user/js/password_strength.js @@ -0,0 +1,39 @@ +(function($) { + // Based on the Password Strength Indictor By Benjamin Sterling + // http://benjaminsterling.com/password-strength-indicator-and-generator/ + $.widget("ui.user_password_strength", { + _init: function() { + var self = this; + $(this.element).keyup(function() { + var strength = self.calculateStrength (this.value); + var index = Math.min(Math.floor( strength / 10 ), 10); + $("#g-password-gauge") + .removeAttr('class') + .addClass( "g-password-strength0" ) + .addClass( self.options.classes[ index ] ); + }).after("<div id='g-password-gauge' class='g-password-strength0'></div>"); + }, + + calculateStrength: function(value) { + // Factor in the length of the password + var strength = Math.min(5, value.length) * 10 - 20; + // Factor in the number of numbers + strength += Math.min(3, value.length - value.replace(/[0-9]/g,"").length) * 10; + // Factor in the number of non word characters + strength += Math.min(3, value.length - value.replace(/\W/g,"").length) * 15; + // Factor in the number of Upper case letters + strength += Math.min(3, value.length - value.replace(/[A-Z]/g,"").length) * 10; + + // Normalizxe between 0 and 100 + return Math.max(0, Math.min(100, strength)); + } + }); + $.extend($.ui.user_password_strength, { + defaults: { + classes : ['g-password-strength10', 'g-password-strength20', 'g-password-strength30', + 'g-password-strength40', 'g-password-strength50', 'g-password-strength60', + 'g-password-strength70',' g-password-strength80',' g-password-strength90', + 'g-password-strength100'] + } + }); + })(jQuery); diff --git a/modules/user/lib/PasswordHash.php b/modules/user/lib/PasswordHash.php new file mode 100644 index 0000000..d6783c0 --- /dev/null +++ b/modules/user/lib/PasswordHash.php @@ -0,0 +1,248 @@ +<?php defined("SYSPATH") or die("No direct script access."); +# +# Portable PHP password hashing framework. +# +# Version 0.1 / genuine. +# +# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in +# the public domain. +# +# There's absolutely no warranty. +# +# The homepage URL for this framework is: +# +# http://www.openwall.com/phpass/ +# +# Please be sure to update the Version line if you edit this file in any way. +# It is suggested that you leave the main version number intact, but indicate +# your project name (after the slash) and add your own revision information. +# +# Please do not change the "private" password hashing method implemented in +# here, thereby making your hashes incompatible. However, if you must, please +# change the hash type identifier (the "$P$") to something different. +# +# Obviously, since this code is in the public domain, the above are not +# requirements (there can be none), but merely suggestions. +# +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime() . getmypid(); + } + + function get_random_bytes($count) + { + $output = ''; + if (($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= + pack('H*', md5($this->random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) == $output) + $output = '*1'; + + if (substr($setting, 0, 3) != '$P$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) != 8) + return $output; + + # We're kind of forced to use MD5 here since it's the only + # cryptographic primitive available in all versions of PHP + # currently in use. To implement our own low-level crypto + # in PHP would result in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + if (PHP_VERSION >= '5') { + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + } else { + $hash = pack('H*', md5($salt . $password)); + do { + $hash = pack('H*', md5($hash . $password)); + } while (--$count); + } + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_extended($input) + { + $count_log2 = min($this->iteration_count_log2 + 8, 24); + # This should be odd to not reveal weak DES keys, and the + # maximum valid value is (2**24 - 1) which is odd anyway. + $count = (1 << $count_log2) - 1; + + $output = '_'; + $output .= $this->itoa64[$count & 0x3f]; + $output .= $this->itoa64[($count >> 6) & 0x3f]; + $output .= $this->itoa64[($count >> 12) & 0x3f]; + $output .= $this->itoa64[($count >> 18) & 0x3f]; + + $output .= $this->encode64($input, 3); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) == 60) + return $hash; + } + + if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) { + if (strlen($random) < 3) + $random = $this->get_random_bytes(3); + $hash = + crypt($password, $this->gensalt_extended($random)); + if (strlen($hash) == 20) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) == 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] == '*') + $hash = crypt($password, $stored_hash); + + return $hash == $stored_hash; + } +} + +?> diff --git a/modules/user/libraries/drivers/IdentityProvider/Gallery.php b/modules/user/libraries/drivers/IdentityProvider/Gallery.php new file mode 100644 index 0000000..67da33d --- /dev/null +++ b/modules/user/libraries/drivers/IdentityProvider/Gallery.php @@ -0,0 +1,164 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* + * Based on the Cache_Sqlite_Driver developed by the Kohana Team + */ +class IdentityProvider_Gallery_Driver implements IdentityProvider_Driver { + /** + * @see IdentityProvider_Driver::guest. + */ + public function guest() { + return user::guest(); + } + + /** + * @see IdentityProvider_Driver::guest. + */ + public function admin_user() { + return user::admin_user(); + } + + /** + * @see IdentityProvider_Driver::create_user. + */ + public function create_user($name, $full_name, $password, $email) { + $user = ORM::factory("user"); + $user->name = $name; + $user->full_name = $full_name; + $user->password = $password; + $user->email = $email; + return $user->save(); + } + + /** + * @see IdentityProvider_Driver::is_correct_password. + */ + public function is_correct_password($user, $password) { + $valid = $user->password; + + // Try phpass first, since that's what we generate. + if (strlen($valid) == 34) { + require_once(MODPATH . "user/lib/PasswordHash.php"); + $hashGenerator = new PasswordHash(10, true); + return $hashGenerator->CheckPassword($password, $valid); + } + + $salt = substr($valid, 0, 4); + // Support both old (G1 thru 1.4.0; G2 thru alpha-4) and new password schemes: + $guess = (strlen($valid) == 32) ? md5($password) : ($salt . md5($salt . $password)); + if (!strcmp($guess, $valid)) { + return true; + } + + // Passwords with <&"> created by G2 prior to 2.1 were hashed with entities + $sanitizedPassword = html::chars($password, false); + $guess = (strlen($valid) == 32) ? md5($sanitizedPassword) + : ($salt . md5($salt . $sanitizedPassword)); + if (!strcmp($guess, $valid)) { + return true; + } + + return false; + } + + /** + * @see IdentityProvider_Driver::lookup_user. + */ + public function lookup_user($id) { + return user::lookup($id); + } + + /** + * @see IdentityProvider_Driver::lookup_user_by_name. + */ + public function lookup_user_by_name($name) { + return user::lookup_by_name($name); + } + + /** + * @see IdentityProvider_Driver::create_group. + */ + public function create_group($name) { + $group = ORM::factory("group"); + $group->name = $name; + return $group->save(); + } + + /** + * @see IdentityProvider_Driver::everybody. + */ + public function everybody() { + return group::everybody(); + } + + /** + * @see IdentityProvider_Driver::registered_users. + */ + public function registered_users() { + return group::registered_users(); + } + + /** + * @see IdentityProvider_Driver::lookup_group. + */ + public function lookup_group($id) { + return group::lookup($id); + } + + /** + * @see IdentityProvider_Driver::lookup_group_by_name. + */ + public function lookup_group_by_name($name) { + return group::lookup_by_name($name); + } + + /** + * @see IdentityProvider_Driver::get_user_list. + */ + public function get_user_list($ids) { + return ORM::factory("user") + ->where("id", "IN", $ids) + ->find_all(); + } + + /** + * @see IdentityProvider_Driver::groups. + */ + public function groups() { + return ORM::factory("group")->find_all(); + } + + /** + * @see IdentityProvider_Driver::add_user_to_group. + */ + public function add_user_to_group($user, $group) { + $group->add($user); + $group->save(); + } + + /** + * @see IdentityProvider_Driver::remove_user_to_group. + */ + public function remove_user_from_group($user, $group) { + $group->remove($user); + $group->save(); + } +} // End Identity Gallery Driver + diff --git a/modules/user/models/group.php b/modules/user/models/group.php new file mode 100644 index 0000000..a5d7c5b --- /dev/null +++ b/modules/user/models/group.php @@ -0,0 +1,89 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Group_Model_Core extends ORM implements Group_Definition { + protected $has_and_belongs_to_many = array("users"); + protected $users_cache = null; + + /** + * @see ORM::delete() + */ + public function delete($id=null) { + $old = clone $this; + module::event("group_before_delete", $this); + parent::delete($id); + + db::build() + ->delete("groups_users") + ->where("group_id", "=", empty($id) ? $old->id : $id) + ->execute(); + + module::event("group_deleted", $old); + $this->users_cache = null; + } + + public function users() { + if (!$this->users_cache) { + $this->users_cache = $this->users->find_all()->as_array(); + } + return $this->users_cache; + } + + /** + * Specify our rules here so that we have access to the instance of this model. + */ + public function validate(Validation $array=null) { + // validate() is recursive, only modify the rules on the outermost call. + if (!$array) { + $this->rules = array( + "name" => array("rules" => array("required", "length[1,255]"), + "callbacks" => array(array($this, "valid_name")))); + } + + parent::validate($array); + } + + public function save() { + if (!$this->loaded()) { + // New group + parent::save(); + module::event("group_created", $this); + } else { + // Updated group + $original = ORM::factory("group", $this->id); + parent::save(); + module::event("group_updated", $original, $this); + } + + $this->users_cache = null; + return $this; + } + + /** + * Validate the user name. Make sure there are no conflicts. + */ + public function valid_name(Validation $v, $field) { + if (db::build()->from("groups") + ->where("name", "=", $this->name) + ->where("id", "<>", $this->id) + ->count_records() == 1) { + $v->add_error("name", "conflict"); + } + } +}
\ No newline at end of file diff --git a/modules/user/models/user.php b/modules/user/models/user.php new file mode 100644 index 0000000..af05c0c --- /dev/null +++ b/modules/user/models/user.php @@ -0,0 +1,185 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class User_Model_Core extends ORM implements User_Definition { + protected $has_and_belongs_to_many = array("groups"); + protected $password_length = null; + protected $groups_cache = null; + + public function __set($column, $value) { + switch ($column) { + case "hashed_password": + $column = "password"; + break; + + case "password": + $this->password_length = strlen($value); + $value = user::hash_password($value); + break; + } + parent::__set($column, $value); + } + + /** + * @see ORM::delete() + */ + public function delete($id=null) { + $old = clone $this; + module::event("user_before_delete", $this); + parent::delete($id); + + db::build() + ->delete("groups_users") + ->where("user_id", "=", empty($id) ? $old->id : $id) + ->execute(); + + module::event("user_deleted", $old); + $this->groups_cache = null; + } + + /** + * Return a url to the user's avatar image. + * @param integer $size the target size of the image (default 80px) + * @return string a url + */ + public function avatar_url($size=80, $default=null) { + return sprintf("http://www.gravatar.com/avatar/%s.jpg?s=%d&r=pg%s", + md5($this->email), $size, $default ? "&d=" . urlencode($default) : ""); + } + + public function groups() { + if (!$this->groups_cache) { + $this->groups_cache = $this->groups->find_all()->as_array(); + } + return $this->groups_cache; + } + + /** + * Specify our rules here so that we have access to the instance of this model. + */ + public function validate(Validation $array=null) { + // validate() is recursive, only modify the rules on the outermost call. + if (!$array) { + $this->rules = array( + "admin" => array("callbacks" => array(array($this, "valid_admin"))), + "email" => array("rules" => array("length[1,255]", "valid::email"), + "callbacks" => array(array($this, "valid_email"))), + "full_name" => array("rules" => array("length[0,255]")), + "locale" => array("rules" => array("length[2,10]")), + "name" => array("rules" => array("length[1,32]", "required"), + "callbacks" => array(array($this, "valid_name"))), + "password" => array("callbacks" => array(array($this, "valid_password"))), + "url" => array("rules" => array("valid::url")), + ); + } + + parent::validate($array); + } + + /** + * Handle any business logic necessary to create or update a user. + * @see ORM::save() + * + * @return ORM User_Model + */ + public function save() { + if ($this->full_name === null) { + $this->full_name = ""; + } + + if (!$this->loaded()) { + // New user + $this->add(group::everybody()); + if (!$this->guest) { + $this->add(group::registered_users()); + } + + parent::save(); + module::event("user_created", $this); + } else { + // Updated user + $original = ORM::factory("user", $this->id); + parent::save(); + module::event("user_updated", $original, $this); + } + + $this->groups_cache = null; + return $this; + } + + /** + * Return the best version of the user's name. Either their specified full name, or fall back + * to the user name. + * @return string + */ + public function display_name() { + return empty($this->full_name) ? $this->name : $this->full_name; + } + + /** + * Validate the user name. Make sure there are no conflicts. + */ + public function valid_name(Validation $v, $field) { + if (db::build()->from("users") + ->where("name", "=", $this->name) + ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null) + ->count_records() == 1) { + $v->add_error("name", "conflict"); + } + } + + /** + * Validate the password. + */ + public function valid_password(Validation $v, $field) { + if ($this->guest) { + return; + } + + if (!$this->loaded() || isset($this->password_length)) { + $minimum_length = module::get_var("user", "minimum_password_length", 5); + if ($this->password_length < $minimum_length) { + $v->add_error("password", "min_length"); + } + } + } + + /** + * Validate the admin bit. + */ + public function valid_admin(Validation $v, $field) { + $active = identity::active_user(); + if ($this->id == $active->id && $active->admin && !$this->admin) { + $v->add_error("admin", "locked"); + } + } + + /** + * Validate the email field. + */ + public function valid_email(Validation $v, $field) { + if ($this->guest) { // guests don't require an email address + return; + } + + if (empty($this->email)) { + $v->add_error("email", "required"); + } + } +} diff --git a/modules/user/module.info b/modules/user/module.info new file mode 100644 index 0000000..d5128db --- /dev/null +++ b/modules/user/module.info @@ -0,0 +1,8 @@ +name = "Users and Groups" +description = "Gallery 3 user and group management" +version = 4 + +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:user" +discuss_url = "http://galleryproject.org/forum_module_user" diff --git a/modules/user/views/admin_users.html.php b/modules/user/views/admin_users.html.php new file mode 100644 index 0000000..033c9da --- /dev/null +++ b/modules/user/views/admin_users.html.php @@ -0,0 +1,142 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<script type="text/javascript"> + var add_user_to_group_url = "<?= url::site("admin/users/add_user_to_group/__USERID__/__GROUPID__?csrf=$csrf") ?>"; + $(document).ready(function(){ + $("#g-user-admin-list .g-core-info").draggable({ + helper: "clone" + }); + $("#g-group-admin .g-group").droppable({ + accept: ".g-core-info", + hoverClass: "g-selected", + drop: function(ev, ui) { + var user_id = $(ui.draggable).attr("id").replace("g-user-", ""); + var group_id = $(this).attr("id").replace("g-group-", ""); + $.get(add_user_to_group_url.replace("__USERID__", user_id).replace("__GROUPID__", group_id), + {}, + function() { + reload_group(group_id); + }); + } + }); + $("#g-group-1").droppable("destroy"); + $("#g-group-2").droppable("destroy"); + }); + + var reload_group = function(group_id) { + var reload_group_url = "<?= url::site("admin/users/group/__GROUPID__") ?>"; + $.get(reload_group_url.replace("__GROUPID__", group_id), + {}, + function(data) { + $("#g-group-" + group_id).html(data); + $("#g-group-" + group_id + " .g-dialog-link").gallery_dialog(); + }); + } + + var remove_user = function(user_id, group_id) { + var remove_user_url = "<?= url::site("admin/users/remove_user_from_group/__USERID__/__GROUPID__?csrf=$csrf") ?>"; + $.get(remove_user_url.replace("__USERID__", user_id).replace("__GROUPID__", group_id), + {}, + function() { + reload_group(group_id); + }); + } +</script> + +<div class="g-block"> + <h1> <?= t("Users and groups") ?> </h1> + + <div class="g-block-content"> + + <div id="g-user-admin" class="g-block"> + <a href="<?= url::site("admin/users/add_user_form") ?>" + class="g-dialog-link g-button g-right ui-icon-left ui-state-default ui-corner-all" + title="<?= t("Create a new user")->for_html_attr() ?>"> + <span class="ui-icon ui-icon-circle-plus"></span> + <?= t("Add a new user") ?> + </a> + + <h2> <?= t("Users") ?> </h2> + + <div class="g-block-content"> + <table id="g-user-admin-list"> + <tr> + <th><?= t("Username") ?></th> + <th><?= t("Full name") ?></th> + <th><?= t("Email") ?></th> + <th><?= t("Last login") ?></th> + <th><?= t("Albums/Photos") ?></th> + <th><?= t("Actions") ?></th> + </tr> + + <? foreach ($users as $i => $user): ?> + <tr id="g-user-<?= $user->id ?>" class="<?= text::alternate("g-odd", "g-even") ?> g-user <?= $user->admin ? "g-admin" : "" ?>"> + <td id="g-user-<?= $user->id ?>" class="g-core-info g-draggable"> + <img src="<?= $user->avatar_url(20, $theme->url("images/avatar.jpg", true)) ?>" + title="<?= t("Drag user onto a group to add as a new member")->for_html_attr() ?>" + alt="<?= html::clean_attribute($user->name) ?>" + width="20" + height="20" /> + <?= html::clean($user->name) ?> + </td> + <td> + <?= html::clean($user->full_name) ?> + </td> + <td> + <?= html::clean($user->email) ?> + </td> + <td> + <?= ($user->last_login == 0) ? "" : gallery::date($user->last_login) ?> + </td> + <td> + <?= db::build()->from("items")->where("owner_id", "=", $user->id)->count_records() ?> + </td> + <td> + <a href="<?= url::site("admin/users/edit_user_form/$user->id") ?>" + open_text="<?= t("Close") ?>" + class="g-panel-link g-button ui-state-default ui-corner-all ui-icon-left"> + <span class="ui-icon ui-icon-pencil"></span><span class="g-button-text"><?= t("Edit") ?></span></a> + <? if (identity::active_user()->id != $user->id && !$user->guest): ?> + <a href="<?= url::site("admin/users/delete_user_form/$user->id") ?>" + class="g-dialog-link g-button ui-state-default ui-corner-all ui-icon-left"> + <span class="ui-icon ui-icon-trash"></span><?= t("Delete") ?></a> + <? else: ?> + <span title="<?= t("This user cannot be deleted")->for_html_attr() ?>" + class="g-button ui-state-disabled ui-corner-all ui-icon-left"> + <span class="ui-icon ui-icon-trash"></span><?= t("Delete") ?></span> + <? endif ?> + </td> + </tr> + <? endforeach ?> + </table> + + <div class="g-paginator"> + <?= $theme->paginator() ?> + </div> + + </div> + </div> + + <div id="g-group-admin" class="g-block ui-helper-clearfix"> + <a href="<?= url::site("admin/users/add_group_form") ?>" + class="g-dialog-link g-button g-right ui-icon-left ui-state-default ui-corner-all" + title="<?= t("Create a new group")->for_html_attr() ?>"> + <span class="ui-icon ui-icon-circle-plus"></span> + <?= t("Add a new group") ?> + </a> + + <h2> <?= t("Groups") ?> </h2> + + <div class="g-block-content"> + <ul> + <? foreach ($groups as $i => $group): ?> + <li id="g-group-<?= $group->id ?>" class="g-group g-left <?= ($group->special ? "g-default-group" : "") ?>"> + <? $v = new View("admin_users_group.html"); $v->group = $group; ?> + <?= $v ?> + </li> + <? endforeach ?> + </ul> + </div> + </div> + + </div> +</div> diff --git a/modules/user/views/admin_users_delete_user.html.php b/modules/user/views/admin_users_delete_user.html.php new file mode 100644 index 0000000..44777ae --- /dev/null +++ b/modules/user/views/admin_users_delete_user.html.php @@ -0,0 +1,7 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div id="g-admin-users-delete-user"> + <p> + <?= t("Really delete <b>%name</b>? Any photos, movies or albums owned by this user will transfer ownership to <b>%new_owner</b>.", array("name" => $user->display_name(), "new_owner" => identity::active_user()->display_name())) ?> + </p> + <?= $form ?> +</div> diff --git a/modules/user/views/admin_users_group.html.php b/modules/user/views/admin_users_group.html.php new file mode 100644 index 0000000..31b9135 --- /dev/null +++ b/modules/user/views/admin_users_group.html.php @@ -0,0 +1,40 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<h4> + <a href="<?= url::site("admin/users/edit_group_form/$group->id") ?>" + title="<?= t("Edit the %name group's name", array("name" => $group->name))->for_html_attr() ?>" + class="g-dialog-link"><?= html::clean($group->name) ?></a> + <? if (!$group->special): ?> + <a href="<?= url::site("admin/users/delete_group_form/$group->id") ?>" + title="<?= t("Delete the %name group", array("name" => $group->name))->for_html_attr() ?>" + class="g-dialog-link g-button g-right"> + <span class="ui-icon ui-icon-trash"><?= t("Delete") ?></span></a> + <? else: ?> + <a title="<?= t("This default group cannot be deleted")->for_html_attr() ?>" + class="g-button g-right ui-state-disabled ui-icon-left"> + <span class="ui-icon ui-icon-trash"><?= t("Delete") ?></span></a> + <? endif ?> +</h4> + +<? if ($group->users->count_all() > 0): ?> +<ul class="g-member-list"> + <? foreach ($group->users->order_by("name", "ASC")->find_all() as $i => $user): ?> + <li class="g-user"> + <?= html::clean($user->name) ?> + <? if (!$group->special): ?> + <a href="javascript:remove_user(<?= $user->id ?>, <?= $group->id ?>)" + class="g-button g-right ui-state-default ui-corner-all ui-icon-left" + title="<?= t("Remove %user from %group group", + array("user" => $user->name, "group" => $group->name))->for_html_attr() ?>"> + <span class="ui-icon ui-icon-closethick"><?= t("Remove") ?></span> + </a> + <? endif ?> + </li> + <? endforeach ?> +</ul> +<? else: ?> +<div> + <p class="ui-state-disabled"> + <?= t("Drag & drop users from the \"Users\" list onto this group to add group members.") ?> + </p> +</div> +<? endif ?> diff --git a/modules/user/views/reset_password.html.php b/modules/user/views/reset_password.html.php new file mode 100644 index 0000000..d939ad4 --- /dev/null +++ b/modules/user/views/reset_password.html.php @@ -0,0 +1,18 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<html> + <head> + <title><?= t("Password reset request") ?> </title> + </head> + <body> + <h2><?= t("Password reset request") ?> </h2> + <p> + <?= t("Hello, %name,", array("name" => $user->full_name ? $user->full_name : $user->name)) ?> + </p> + <p> + <?= t("We received a request to reset your password for <a href=\"%site_url\">%base_url</a>. If you made this request, you can confirm it by <a href=\"%confirm_url\">clicking this link</a>. If you didn't request this password reset, it's ok to ignore this mail.", + array("site_url" => html::mark_clean(url::abs_site("/")), + "base_url" => html::mark_clean(url::base(false)), + "confirm_url" => $confirm_url)) ?> + </p> + </body> +</html> diff --git a/modules/watermark/controllers/admin_watermarks.php b/modules/watermark/controllers/admin_watermarks.php new file mode 100644 index 0000000..222279e --- /dev/null +++ b/modules/watermark/controllers/admin_watermarks.php @@ -0,0 +1,158 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class Admin_Watermarks_Controller extends Admin_Controller { + public function index() { + $name = module::get_var("watermark", "name"); + + $view = new Admin_View("admin.html"); + $view->page_title = t("Watermarks"); + $view->content = new View("admin_watermarks.html"); + if ($name) { + $view->content->name = module::get_var("watermark", "name"); + $view->content->url = url::file("var/modules/watermark/$name"); + $view->content->width = module::get_var("watermark", "width"); + $view->content->height = module::get_var("watermark", "height"); + $view->content->position = module::get_var("watermark", "position"); + } + print $view; + } + + public function form_edit() { + print watermark::get_edit_form(); + } + + public function edit() { + access::verify_csrf(); + + $form = watermark::get_edit_form(); + if ($form->validate()) { + module::set_var("watermark", "position", $form->edit_watermark->position->value); + module::set_var("watermark", "transparency", $form->edit_watermark->transparency->value); + $this->_update_graphics_rules(); + + log::success("watermark", t("Watermark changed")); + message::success(t("Watermark changed")); + json::reply( + array("result" => "success", + "location" => url::site("admin/watermarks"))); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_delete() { + print watermark::get_delete_form(); + } + + public function delete() { + access::verify_csrf(); + + $form = watermark::get_delete_form(); + if ($form->validate()) { + if ($name = basename(module::get_var("watermark", "name"))) { + system::delete_later(VARPATH . "modules/watermark/$name"); + + module::clear_var("watermark", "name"); + module::clear_var("watermark", "width"); + module::clear_var("watermark", "height"); + module::clear_var("watermark", "mime_type"); + module::clear_var("watermark", "position"); + $this->_update_graphics_rules(); + + log::success("watermark", t("Watermark deleted")); + message::success(t("Watermark deleted")); + } + json::reply(array("result" => "success", "location" => url::site("admin/watermarks"))); + } else { + json::reply(array("result" => "error", "html" => (string)$form)); + } + } + + public function form_add() { + print watermark::get_add_form(); + } + + public function add() { + access::verify_csrf(); + + $form = watermark::get_add_form(); + // For TEST_MODE, we want to simulate a file upload. Because this is not a true upload, Forge's + // validation logic will correctly reject it. So, we skip validation when we're running tests. + if (TEST_MODE || $form->validate()) { + $file = $_POST["file"]; + // Forge prefixes files with "uploadfile-xxxxxxx" for uniqueness + $name = preg_replace("/uploadfile-[^-]+-(.*)/", '$1', basename($file)); + + try { + list ($width, $height, $mime_type, $extension) = photo::get_file_metadata($file); + // Sanitize filename, which ensures a valid extension. This renaming prevents the issues + // addressed in ticket #1855, where an image that looked valid (header said jpg) with a + // php extension was previously accepted without changing its extension. + $name = legal_file::sanitize_filename($name, $extension, "photo"); + } catch (Exception $e) { + message::error(t("Invalid or unidentifiable image file")); + system::delete_later($file); + return; + } + + rename($file, VARPATH . "modules/watermark/$name"); + module::set_var("watermark", "name", $name); + module::set_var("watermark", "width", $width); + module::set_var("watermark", "height", $height); + module::set_var("watermark", "mime_type", $mime_type); + module::set_var("watermark", "position", $form->add_watermark->position->value); + module::set_var("watermark", "transparency", $form->add_watermark->transparency->value); + $this->_update_graphics_rules(); + system::delete_later($file); + + message::success(t("Watermark saved")); + log::success("watermark", t("Watermark saved")); + json::reply(array("result" => "success", "location" => url::site("admin/watermarks"))); + } else { + // rawurlencode the results because the JS code that uploads the file buffers it in an + // iframe which entitizes the HTML and makes it difficult for the JS to process. If we url + // encode it now, it passes through cleanly. See ticket #797. + json::reply(array("result" => "error", "html" => rawurlencode((string)$form))); + } + + // Override the application/json mime type. The dialog based HTML uploader uses an iframe to + // buffer the reply, and on some browsers (Firefox 3.6) it does not know what to do with the + // JSON that it gets back so it puts up a dialog asking the user what to do with it. So force + // the encoding type back to HTML for the iframe. + // See: http://jquery.malsup.com/form/#file-upload + header("Content-Type: text/html; charset=" . Kohana::CHARSET); + } + + private function _update_graphics_rules() { + graphics::remove_rules("watermark"); + if ($name = module::get_var("watermark", "name")) { + foreach (array("thumb", "resize") as $target) { + graphics::add_rule( + "watermark", $target, "gallery_graphics::composite", + array("file" => VARPATH . "modules/watermark/$name", + "width" => module::get_var("watermark", "width"), + "height" => module::get_var("watermark", "height"), + "position" => module::get_var("watermark", "position"), + "transparency" => 101 - module::get_var("watermark", "transparency")), + 1000); + } + } + } +}
\ No newline at end of file diff --git a/modules/watermark/helpers/watermark.php b/modules/watermark/helpers/watermark.php new file mode 100644 index 0000000..3357c14 --- /dev/null +++ b/modules/watermark/helpers/watermark.php @@ -0,0 +1,82 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class watermark_Core { + static function get_add_form() { + for ($i = 1; $i <= 100; $i++) { + $range[$i] = "$i%"; + } + + $form = new Forge("admin/watermarks/add", "", "post", array("id" => "g-add-watermark-form")); + $group = $form->group("add_watermark")->label(t("Upload watermark")); + $group->upload("file")->label(t("Watermark"))->rules("allow[jpg,png,gif]|size[1MB]|required") + ->error_messages("required", "You must select a watermark") + ->error_messages("invalid_type", "The watermark must be a JPG, GIF or PNG") + ->error_messages("max_size", "The watermark is too big (1 MB max)"); + $group->dropdown("position")->label(t("Watermark position")) + ->options(self::positions()) + ->selected("southeast"); + $group->dropdown("transparency")->label(t("Transparency (100% = completely transparent)")) + ->options($range) + ->selected(1); + $group->submit("")->value(t("Upload")); + return $form; + } + + static function get_edit_form() { + for ($i = 1; $i <= 100; $i++) { + $range[$i] = "$i%"; + } + + $form = new Forge("admin/watermarks/edit", "", "post", array("id" => "g-edit-watermark-form")); + $group = $form->group("edit_watermark")->label(t("Edit Watermark")); + $group->dropdown("position")->label(t("Watermark Position")) + ->options(self::positions()) + ->selected(module::get_var("watermark", "position")); + $group->dropdown("transparency")->label(t("Transparency (100% = completely transparent)")) + ->options($range) + ->selected(module::get_var("watermark", "transparency")); + $group->submit("")->value(t("Save")); + return $form; + } + + static function get_delete_form() { + $form = new Forge("admin/watermarks/delete", "", "post", array("id" => "g-delete-watermark-form")); + $group = $form->group("delete_watermark")->label(t("Really delete Watermark?")); + $group->submit("")->value(t("Delete")); + return $form; + } + + static function positions() { + return array("northwest" => t("Northwest"), + "north" => t("North"), + "northeast" => t("Northeast"), + "west" => t("West"), + "center" => t("Center"), + "east" => t("East"), + "southwest" => t("Southwest"), + "south" => t("South"), + "southeast" => t("Southeast")); + } + + static function position($key) { + $positions = self::positions(); + return $positions[$key]; + } +}
\ No newline at end of file diff --git a/modules/watermark/helpers/watermark_event.php b/modules/watermark/helpers/watermark_event.php new file mode 100644 index 0000000..7547515 --- /dev/null +++ b/modules/watermark/helpers/watermark_event.php @@ -0,0 +1,29 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class watermark_event_Core { + static function admin_menu($menu, $theme) { + $menu->get("content_menu") + ->append( + Menu::factory("link") + ->id("watermarks") + ->label(t("Watermarks")) + ->url(url::site("admin/watermarks"))); + } +} diff --git a/modules/watermark/helpers/watermark_installer.php b/modules/watermark/helpers/watermark_installer.php new file mode 100644 index 0000000..1333891 --- /dev/null +++ b/modules/watermark/helpers/watermark_installer.php @@ -0,0 +1,42 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2013 Bharat Mediratta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class watermark_installer { + static function install() { + $db = Database::instance(); + $db->query("CREATE TABLE IF NOT EXISTS {watermarks} ( + `id` int(9) NOT NULL auto_increment, + `name` varchar(32) NOT NULL, + `width` int(9) NOT NULL, + `height` int(9) NOT NULL, + `active` boolean default 0, + `position` boolean default 0, + `mime_type` varchar(64) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`name`)) + DEFAULT CHARSET=utf8;"); + + @mkdir(VARPATH . "modules/watermark"); + } + + static function uninstall() { + Database::instance()->query("DROP TABLE {watermarks}"); + dir::unlink(VARPATH . "modules/watermark"); + } +} diff --git a/modules/watermark/module.info b/modules/watermark/module.info new file mode 100644 index 0000000..e5003cd --- /dev/null +++ b/modules/watermark/module.info @@ -0,0 +1,7 @@ +name = "Watermarks" +description = "Allows users to watermark their photos" +version = 2 +author_name = "Gallery Team" +author_url = "http://codex.galleryproject.org/Gallery:Team" +info_url = "http://codex.galleryproject.org/Gallery3:Modules:watermark" +discuss_url = "http://galleryproject.org/forum_module_watermark" diff --git a/modules/watermark/views/admin_watermarks.html.php b/modules/watermark/views/admin_watermarks.html.php new file mode 100644 index 0000000..fc634b8 --- /dev/null +++ b/modules/watermark/views/admin_watermarks.html.php @@ -0,0 +1,39 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> +<div class="g-block"> + <h1> <?= t("Watermarks") ?> </h1> + <p> + <?= t("You can have one watermark for your Gallery. This watermark will be applied to all thumbnails and resized images, but it will not be applied to your full size images. To make sure that your guests can only see watermarked images, you should restrict access to your full size images.") ?> + </p> + + <div class="g-block-content"> + <? if (empty($name)): ?> + <a href="<?= url::site("admin/watermarks/form_add") ?>" + title="<?= t("Upload a watermark")->for_html_attr() ?>" + class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all"><span class="ui-icon ui-icon-document-b"></span><?= t("Upload a watermark") ?></a> + <? else: ?> + <h2> <?= t("Active watermark") ?> </h2> + <p> + <?= t("Note that changing this watermark will require you to rebuild all of your thumbnails and resized images.") ?> + </p> + <div> + <div class="g-photo"> + <img width="<?= $width ?>" height="<?= $height ?>" src="<?= $url ?>" /> + <p> + <?= t("Position: %position", array("position" => watermark::position($position))) ?> + </p> + <p> + <?= t("Transparency: %transparency%", array("transparency" => module::get_var("watermark", "transparency"))) ?> + </p> + </div> + <div class="controls"> + <a href="<?= url::site("admin/watermarks/form_edit") ?>" + title="<?= t("Edit watermark")->for_html_attr() ?>" + class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all"><span class="ui-icon ui-icon-pencil"></span><?= t("edit") ?></a> + <a href="<?= url::site("admin/watermarks/form_delete") ?>" + title="<?= t("Delete watermark")->for_html_attr() ?>" + class="g-dialog-link g-button ui-icon-left ui-state-default ui-corner-all"><span class="ui-icon ui-icon-trash"></span><?= t("delete") ?></a> + </div> + </div> + <? endif ?> + </div> +</div> |
