From 88475b0f05218464a5fad942706877dd4b005228 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 10 Sep 2016 15:47:35 +0100 Subject: [PATCH] Initial commit based on Aviary core --- .gitignore | 5 + .htaccess | 47 + blocks/.htaccess | 1 + blocks/ORB/Login.pm | 1289 ++++++++++ blocks/ORB/Userbar.pm | 122 + index.cgi | 52 + lang/en/aviary.lang | 1 + lang/en/calendar.lang | 10 + lang/en/debug.lang | 5 + lang/en/global.lang | 55 + lang/en/import.lang | 13 + lang/en/login.lang | 175 ++ lang/en/navigation.lang | 6 + lang/en/permission.lang | 4 + lang/en/userbar.lang | 14 + lang/en/validate.lang | 18 + makedocs.sh | 7 + modules/.htaccess | 1 + modules/ORB.pm | 773 ++++++ modules/ORB/AppUser.pm | 197 ++ modules/ORB/BlockSelector.pm | 115 + modules/ORB/System.pm | 70 + modules/ORB/System/Entity.pm | 376 +++ modules/ORB/System/Metadata.pm | 369 +++ modules/ORB/System/Roles.pm | 883 +++++++ modules/ORB/System/Tags.pm | 512 ++++ supportfiles/.htaccess | 1 + supportfiles/Doxyfile | 2282 +++++++++++++++++ supportfiles/DoxygenLayout.xml | 194 ++ supportfiles/SendMessages.pl | 51 + supportfiles/customdoxygen.css | 1029 ++++++++ supportfiles/lang_to_db.pl | 157 ++ supportfiles/perlmod.deps | 35 + templates/default/api/html_error.tem | 1 + templates/default/api/html_wrapper.tem | 7 + templates/default/css/body.css | 43 + templates/default/css/button.css | 167 ++ templates/default/css/controls.css | 31 + templates/default/css/core.css | 333 +++ templates/default/css/error.css | 29 + templates/default/css/hotimage.css | 6 + templates/default/css/inputglow.css | 6 + templates/default/css/login.css | 91 + templates/default/css/messagebox.css | 46 + templates/default/css/notebox.css | 43 + templates/default/css/orb.css | 47 + templates/default/css/shadowbox.css | 40 + templates/default/css/userbar.css | 249 ++ templates/default/error/error_box.tem | 6 + templates/default/error/error_item.tem | 1 + templates/default/error/error_list.tem | 2 + templates/default/error/general.tem | 33 + .../default/images/calendar/addtweet.png | Bin 0 -> 125 bytes templates/default/images/calendar/next.png | Bin 0 -> 174 bytes .../default/images/calendar/previous.png | Bin 0 -> 182 bytes templates/default/images/control_sprites.png | Bin 0 -> 6074 bytes templates/default/images/datepicker.png | Bin 0 -> 3299 bytes templates/default/images/error.png | Bin 0 -> 3259 bytes templates/default/images/favicon.png | 0 templates/default/images/feed-icon-14.png | Bin 0 -> 689 bytes templates/default/images/important.png | Bin 0 -> 3078 bytes templates/default/images/info.png | Bin 0 -> 3829 bytes templates/default/images/logo.png | Bin 0 -> 18286 bytes .../default/images/messages/articleok22.png | Bin 0 -> 4121 bytes templates/default/images/messages/error22.png | Bin 0 -> 995 bytes .../default/images/messages/imported22.png | Bin 0 -> 1102 bytes .../images/messages/permission_error22.png | Bin 0 -> 4048 bytes .../default/images/messages/security22.png | Bin 0 -> 3997 bytes templates/default/images/mode_sprites.png | Bin 0 -> 4063 bytes templates/default/images/slideknob.png | Bin 0 -> 584 bytes templates/default/images/spinner.gif | Bin 0 -> 1849 bytes .../default/images/tree_closed-hover.png | Bin 0 -> 418 bytes templates/default/images/tree_closed.png | Bin 0 -> 434 bytes templates/default/images/tree_open-hover.png | Bin 0 -> 425 bytes templates/default/images/tree_open.png | Bin 0 -> 427 bytes templates/default/images/tree_sprites.png | Bin 0 -> 651 bytes .../default/js/Locale.en-GB.DatePicker.js | 19 + templates/default/js/TabPane.js | 108 + templates/default/js/api.js | 66 + templates/default/js/mootools-core.js | 527 ++++ templates/default/js/mootools-more.js | 472 ++++ templates/default/js/userbar.js | 1 + .../default/lightface/LightFace.Request.js | 59 + templates/default/lightface/LightFace.css | 90 + templates/default/lightface/LightFace.js | 339 +++ templates/default/lightface/LightFaceMod.js | 332 +++ templates/default/lightface/images/b.png | Bin 0 -> 84 bytes templates/default/lightface/images/bl.png | Bin 0 -> 124 bytes templates/default/lightface/images/br.png | Bin 0 -> 124 bytes templates/default/lightface/images/button.png | Bin 0 -> 841 bytes .../default/lightface/images/spinner.gif | Bin 0 -> 2545 bytes templates/default/lightface/images/tl.png | Bin 0 -> 132 bytes templates/default/lightface/images/tr.png | Bin 0 -> 125 bytes templates/default/lightface/version | 141 + templates/default/login/act_error.tem | 2 + templates/default/login/act_form.tem | 19 + templates/default/login/email_actcode.tem | 19 + templates/default/login/email_lockout.tem | 18 + templates/default/login/email_recover.tem | 9 + templates/default/login/email_registered.tem | 19 + templates/default/login/email_reset.tem | 13 + templates/default/login/error.tem | 2 + templates/default/login/error_box.tem | 6 + templates/default/login/failed.tem | 3 + templates/default/login/force_password.tem | 39 + templates/default/login/form.tem | 44 + templates/default/login/lockedout.tem | 1 + templates/default/login/login_warn.tem | 2 + templates/default/login/no_selfreg.tem | 0 templates/default/login/page.tem | 29 + templates/default/login/passchange_error.tem | 1 + templates/default/login/passchange_errors.tem | 4 + templates/default/login/policy.tem | 1 + templates/default/login/recover_error.tem | 2 + templates/default/login/recover_form.tem | 18 + templates/default/login/reg_error.tem | 1 + templates/default/login/reg_errorlist.tem | 5 + templates/default/login/resend_error.tem | 2 + templates/default/login/resend_form.tem | 18 + templates/default/login/selfreg.js | 7 + templates/default/login/selfreg.tem | 34 + templates/default/login/warning_box.tem | 6 + templates/default/messagebox.tem | 13 + templates/default/messagebox_button.tem | 1 + templates/default/messagebox_buttonbar.tem | 3 + templates/default/page.tem | 64 + templates/default/refreshmeta.tem | 1 + .../default/userbar/doclink_disabled.tem | 0 templates/default/userbar/doclink_enabled.tem | 1 + .../default/userbar/images/documentation.png | Bin 0 -> 3642 bytes templates/default/userbar/images/import.png | Bin 0 -> 3342 bytes .../default/userbar/images/important.png | Bin 0 -> 3575 bytes templates/default/userbar/images/settings.png | Bin 0 -> 974 bytes .../default/userbar/images/site-logo.png | Bin 0 -> 3098 bytes templates/default/userbar/images/warning.png | Bin 0 -> 921 bytes templates/default/userbar/import_disabled.tem | 0 templates/default/userbar/import_enabled.tem | 1 + .../default/userbar/profile_loggedin.tem | 17 + .../default/userbar/profile_loggedout.tem | 15 + .../userbar/profile_loggedout_http.tem | 3 + .../userbar/profile_loggedout_https.tem | 16 + templates/default/userbar/userbar.tem | 13 + templates/default/validator_harness.tem | 10 + 143 files changed, 12686 insertions(+) create mode 100755 .gitignore create mode 100755 .htaccess create mode 100755 blocks/.htaccess create mode 100755 blocks/ORB/Login.pm create mode 100644 blocks/ORB/Userbar.pm create mode 100755 index.cgi create mode 100755 lang/en/aviary.lang create mode 100644 lang/en/calendar.lang create mode 100755 lang/en/debug.lang create mode 100755 lang/en/global.lang create mode 100644 lang/en/import.lang create mode 100755 lang/en/login.lang create mode 100755 lang/en/navigation.lang create mode 100755 lang/en/permission.lang create mode 100644 lang/en/userbar.lang create mode 100755 lang/en/validate.lang create mode 100755 makedocs.sh create mode 100755 modules/.htaccess create mode 100755 modules/ORB.pm create mode 100755 modules/ORB/AppUser.pm create mode 100755 modules/ORB/BlockSelector.pm create mode 100755 modules/ORB/System.pm create mode 100644 modules/ORB/System/Entity.pm create mode 100755 modules/ORB/System/Metadata.pm create mode 100755 modules/ORB/System/Roles.pm create mode 100755 modules/ORB/System/Tags.pm create mode 100755 supportfiles/.htaccess create mode 100644 supportfiles/Doxyfile create mode 100644 supportfiles/DoxygenLayout.xml create mode 100755 supportfiles/SendMessages.pl create mode 100644 supportfiles/customdoxygen.css create mode 100755 supportfiles/lang_to_db.pl create mode 100755 supportfiles/perlmod.deps create mode 100755 templates/default/api/html_error.tem create mode 100755 templates/default/api/html_wrapper.tem create mode 100755 templates/default/css/body.css create mode 100755 templates/default/css/button.css create mode 100755 templates/default/css/controls.css create mode 100755 templates/default/css/core.css create mode 100755 templates/default/css/error.css create mode 100755 templates/default/css/hotimage.css create mode 100755 templates/default/css/inputglow.css create mode 100755 templates/default/css/login.css create mode 100755 templates/default/css/messagebox.css create mode 100755 templates/default/css/notebox.css create mode 100755 templates/default/css/orb.css create mode 100755 templates/default/css/shadowbox.css create mode 100755 templates/default/css/userbar.css create mode 100755 templates/default/error/error_box.tem create mode 100755 templates/default/error/error_item.tem create mode 100755 templates/default/error/error_list.tem create mode 100755 templates/default/error/general.tem create mode 100755 templates/default/images/calendar/addtweet.png create mode 100755 templates/default/images/calendar/next.png create mode 100755 templates/default/images/calendar/previous.png create mode 100755 templates/default/images/control_sprites.png create mode 100755 templates/default/images/datepicker.png create mode 100755 templates/default/images/error.png create mode 100755 templates/default/images/favicon.png create mode 100755 templates/default/images/feed-icon-14.png create mode 100755 templates/default/images/important.png create mode 100755 templates/default/images/info.png create mode 100755 templates/default/images/logo.png create mode 100755 templates/default/images/messages/articleok22.png create mode 100755 templates/default/images/messages/error22.png create mode 100755 templates/default/images/messages/imported22.png create mode 100755 templates/default/images/messages/permission_error22.png create mode 100755 templates/default/images/messages/security22.png create mode 100755 templates/default/images/mode_sprites.png create mode 100755 templates/default/images/slideknob.png create mode 100755 templates/default/images/spinner.gif create mode 100755 templates/default/images/tree_closed-hover.png create mode 100755 templates/default/images/tree_closed.png create mode 100755 templates/default/images/tree_open-hover.png create mode 100755 templates/default/images/tree_open.png create mode 100755 templates/default/images/tree_sprites.png create mode 100755 templates/default/js/Locale.en-GB.DatePicker.js create mode 100755 templates/default/js/TabPane.js create mode 100755 templates/default/js/api.js create mode 100755 templates/default/js/mootools-core.js create mode 100755 templates/default/js/mootools-more.js create mode 100755 templates/default/js/userbar.js create mode 100755 templates/default/lightface/LightFace.Request.js create mode 100755 templates/default/lightface/LightFace.css create mode 100755 templates/default/lightface/LightFace.js create mode 100755 templates/default/lightface/LightFaceMod.js create mode 100755 templates/default/lightface/images/b.png create mode 100755 templates/default/lightface/images/bl.png create mode 100755 templates/default/lightface/images/br.png create mode 100755 templates/default/lightface/images/button.png create mode 100755 templates/default/lightface/images/spinner.gif create mode 100755 templates/default/lightface/images/tl.png create mode 100755 templates/default/lightface/images/tr.png create mode 100755 templates/default/lightface/version create mode 100755 templates/default/login/act_error.tem create mode 100755 templates/default/login/act_form.tem create mode 100755 templates/default/login/email_actcode.tem create mode 100755 templates/default/login/email_lockout.tem create mode 100755 templates/default/login/email_recover.tem create mode 100755 templates/default/login/email_registered.tem create mode 100755 templates/default/login/email_reset.tem create mode 100755 templates/default/login/error.tem create mode 100755 templates/default/login/error_box.tem create mode 100755 templates/default/login/failed.tem create mode 100755 templates/default/login/force_password.tem create mode 100755 templates/default/login/form.tem create mode 100755 templates/default/login/lockedout.tem create mode 100755 templates/default/login/login_warn.tem create mode 100755 templates/default/login/no_selfreg.tem create mode 100755 templates/default/login/page.tem create mode 100755 templates/default/login/passchange_error.tem create mode 100755 templates/default/login/passchange_errors.tem create mode 100755 templates/default/login/policy.tem create mode 100755 templates/default/login/recover_error.tem create mode 100755 templates/default/login/recover_form.tem create mode 100755 templates/default/login/reg_error.tem create mode 100755 templates/default/login/reg_errorlist.tem create mode 100755 templates/default/login/resend_error.tem create mode 100755 templates/default/login/resend_form.tem create mode 100755 templates/default/login/selfreg.js create mode 100755 templates/default/login/selfreg.tem create mode 100755 templates/default/login/warning_box.tem create mode 100755 templates/default/messagebox.tem create mode 100755 templates/default/messagebox_button.tem create mode 100755 templates/default/messagebox_buttonbar.tem create mode 100755 templates/default/page.tem create mode 100755 templates/default/refreshmeta.tem create mode 100644 templates/default/userbar/doclink_disabled.tem create mode 100644 templates/default/userbar/doclink_enabled.tem create mode 100755 templates/default/userbar/images/documentation.png create mode 100755 templates/default/userbar/images/import.png create mode 100755 templates/default/userbar/images/important.png create mode 100755 templates/default/userbar/images/settings.png create mode 100755 templates/default/userbar/images/site-logo.png create mode 100755 templates/default/userbar/images/warning.png create mode 100644 templates/default/userbar/import_disabled.tem create mode 100644 templates/default/userbar/import_enabled.tem create mode 100644 templates/default/userbar/profile_loggedin.tem create mode 100755 templates/default/userbar/profile_loggedout.tem create mode 100644 templates/default/userbar/profile_loggedout_http.tem create mode 100644 templates/default/userbar/profile_loggedout_https.tem create mode 100644 templates/default/userbar/userbar.tem create mode 100755 templates/default/validator_harness.tem diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ef8c513 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +autodocs/ +config/*.cfg +.emacs.desktop +.emacs.desktop.lock diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..be6f33b --- /dev/null +++ b/.htaccess @@ -0,0 +1,47 @@ +# Example .htaccess for apache webservers. + +# Uncomment the following three lines if you want your webapp to force HTTPS +# RewriteEngine On +# RewriteCond %{HTTPS} off +# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} + +# Pass pathinfo and query string to the index script +AcceptPathInfo on +RewriteEngine On + +# If the installation is in a subdirectory, add a rewritebase rule for the subdir +# RewriteBase /subdir/ +RewriteBase /orb/ + +RewriteCond %{REQUEST_URI} !^/orb/(templates|media|docs|ckeditor|images|index.cgi) +RewriteRule (.*) index.cgi/$1 [PT,L] + +# Compress text, html, javascript, css, xml: +AddOutputFilterByType DEFLATE text/plain +AddOutputFilterByType DEFLATE text/html +AddOutputFilterByType DEFLATE text/xml +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE application/xml +AddOutputFilterByType DEFLATE application/xhtml+xml +AddOutputFilterByType DEFLATE application/rss+xml +AddOutputFilterByType DEFLATE application/javascript +AddOutputFilterByType DEFLATE application/x-javascript + +# For extra efficiency, make sure cache expiration times are set for content. +# For example, add the following to the webapp's : +# +# ExpiresActive On +# ExpiresDefault "access plus 300 seconds" +# +# And the followin on its : +# +# ExpiresByType text/html "access plus 30 minutes" +# ExpiresByType text/css "access plus 1 day" +# ExpiresByType text/javascript "access plus 1 day" +# ExpiresByType image/gif "access plus 1 month" +# ExpiresByType image/jpeg "access plus 1 month" +# ExpiresByType image/jpg "access plus 1 month" +# ExpiresByType image/png "access plus 1 month" +# ExpiresByType application/x-shockwave-flash "access plus 1 day" +# ExpiresByType application/x-javascript "access plus 1 day" +# ExpiresByType application/x-icon "access plus 1 day" diff --git a/blocks/.htaccess b/blocks/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/blocks/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/blocks/ORB/Login.pm b/blocks/ORB/Login.pm new file mode 100755 index 0000000..0e7f938 --- /dev/null +++ b/blocks/ORB/Login.pm @@ -0,0 +1,1289 @@ +## @file +# This file contains the implementation of the login/logout facility. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +## @class +# A 'stand alone' login implementation. This presents the user with a +# login form, checks the credentials they enter, and then redirects +# them back to the task they were performing that required a login. +package ORB::Login; + +use strict; +use parent qw(ORB); # This class extends the ORB block class +use experimental qw(smartmatch); +use Webperl::Utils qw(path_join is_defined_numeric); +use v5.12; + +# ============================================================================ +# Emailer functions + +## @method $ register_email($user, $password) +# Send a registration welcome message to the specified user. This send an email +# to the user including their username, password, and a link to the activation +# page for their account. +# +# @param user A reference to a user record hash. +# @param password The unencrypted version of the password set for the user. +# @return undef on success, otherwise an error message. +sub register_email { + my $self = shift; + my $user = shift; + my $password = shift; + + # Build URLs to place in the email. + my $acturl = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [], + "params" => "actcode=".$user -> {"act_code"}); + my $actform = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [ "activate" ]); + + my $status = $self -> {"messages"} -> queue_message(subject => $self -> {"template"} -> replace_langvar("LOGIN_REG_SUBJECT"), + message => $self -> {"template"} -> load_template("login/email_registered.tem", + {"***username***" => $user -> {"username"}, + "***password***" => $password, + "***act_code***" => $user -> {"act_code"}, + "***act_url***" => $acturl, + "***act_form***" => $actform, + }), + recipients => [ $user -> {"user_id"} ], + send_immediately => 1); + return ($status ? undef : $self -> {"messages"} -> errstr()); +} + + +## @method $ lockout_email($user, $password, $actcode, $faillimit) +# Send a message to a user informing them that their account has been locked, and +# they need to reactivate it. +# +# @param user A reference to a user record hash. +# @param password The unencrypted version of the password set for the user. +# @param actcode The activation code set for the account. +# @param faillimit The number of login failures the user can have. +# @return undef on success, otherwise an error message. +sub lockout_email { + my $self = shift; + my $user = shift; + my $password = shift; + my $actcode = shift; + my $faillimit = shift; + + # Build URLs to place in the email. + my $acturl = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [], + "params" => "actcode=".$actcode); + my $actform = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [ "activate" ]); + + my $status = $self -> {"messages"} -> queue_message(subject => $self -> {"template"} -> replace_langvar("LOGIN_LOCKOUT_SUBJECT"), + message => $self -> {"template"} -> load_template("login/email_lockout.tem", + {"***username***" => $user -> {"username"}, + "***password***" => $password, + "***act_code***" => $actcode, + "***act_url***" => $acturl, + "***act_form***" => $actform, + "***faillimit***" => $faillimit, + }), + recipients => [ $user -> {"user_id"} ], + send_immediately => 1); + return ($status ? undef : $self -> {"messages"} -> errstr()); +} + + +## @method $ resend_act_email($user, $password) +# Send another copy of the user's activation code to their email address. +# +# @param user A reference to a user record hash. +# @param password The unencrypted version of the password set for the user. +# @return undef on success, otherwise an error message. +sub resend_act_email { + my $self = shift; + my $user = shift; + my $password = shift; + + # Build URLs to place in the email. + my $acturl = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [], + "params" => "actcode=".$user -> {"act_code"}); + my $actform = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [ "activate" ]); + + my $status = $self -> {"messages"} -> queue_message(subject => $self -> {"template"} -> replace_langvar("LOGIN_RESEND_SUBJECT"), + message => $self -> {"template"} -> load_template("login/email_actcode.tem", + {"***username***" => $user -> {"username"}, + "***password***" => $password, + "***act_code***" => $user -> {"act_code"}, + "***act_url***" => $acturl, + "***act_form***" => $actform, + }), + recipients => [ $user -> {"user_id"} ], + send_immediately => 1); + return ($status ? undef : $self -> {"messages"} -> errstr()); +} + + +## @method $ recover_email$user, $actcode) +# Send a copy of the user's username and new actcode to their email address. +# +# @param user A reference to a user record hash. +# @param actcode The unencrypted version of the actcode set for the user. +# @return undef on success, otherwise an error message. +sub recover_email { + my $self = shift; + my $user = shift; + my $actcode = shift; + + # Build URLs to place in the email. + my $reseturl = $self -> build_url("fullurl" => 1, + "block" => "login", + "params" => { "uid" => $user -> {"user_id"}, + "resetcode" => $actcode}, + "joinstr" => "&", + "pathinfo" => []); + + my $status = $self -> {"messages"} -> queue_message(subject => $self -> {"template"} -> replace_langvar("LOGIN_RECOVER_SUBJECT"), + message => $self -> {"template"} -> load_template("login/email_recover.tem", + {"***username***" => $user -> {"username"}, + "***reset_url***" => $reseturl, + }), + recipients => [ $user -> {"user_id"} ], + send_immediately => 1); + return ($status ? undef : $self -> {"messages"} -> errstr()); +} + + +## @method $ reset_email($user, $password) +# Send the user's username and random reset password to them +# +# @param user A reference to a user record hash. +# @param password The unencrypted version of the password set for the user. +# @return undef on success, otherwise an error message. +sub reset_email { + my $self = shift; + my $user = shift; + my $password = shift; + + # Build URLs to place in the email. + my $loginform = $self -> build_url("fullurl" => 1, + "block" => "login", + "pathinfo" => [ ]); + + my $status = $self -> {"messages"} -> queue_message(subject => $self -> {"template"} -> replace_langvar("LOGIN_RESET_SUBJECT"), + message => $self -> {"template"} -> load_template("login/email_reset.tem", + {"***username***" => $user -> {"username"}, + "***password***" => $password, + "***login_url***" => $loginform, + }), + recipients => [ $user -> {"user_id"} ], + send_immediately => 1); + return ($status ? undef : $self -> {"messages"} -> errstr()); +} + + +# ============================================================================ +# Validation functions + +## @method private @ validate_login() +# Determine whether the username and password provided by the user are valid. If +# they are, return the user's data. +# +# @note This does not check for password force change status, as the call mechanics +# do not support the behaviour needed to prompt the user for a change here. +# The caller must check whether a change is needed after fixing the user's session. +# +# @return An array of two values: A reference to the user's data on success, +# or an error string if the login failed, and a reference to a hash of +# arguments that passed validation. +sub validate_login { + my $self = shift; + my $error = ""; + my $args = {}; + + # Check that the username is provided and valid + ($args -> {"username"}, $error) = $self -> validate_string("username", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_USERNAME"), + "minlen" => 2, + "maxlen" => 32, + "formattest" => '^[-\w]+$', + "formatdesc" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADUSERCHAR")}); + # Bomb out at this point if the username is not valid. + return ($self -> {"template"} -> load_template("login/error.tem", {"***reason***" => $error}), $args) + if($error); + + # Do the same with the password... + ($args -> {"password"}, $error) = $self -> validate_string("password", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_PASSWORD"), + "minlen" => 2, + "maxlen" => 255}); + return ($self -> {"template"} -> load_template("login/error.tem", {"***reason***" => $error}), $args) + if($error); + + # Username and password appear to be present and contain sane characters. Try to log the user in... + my $user = $self -> {"session"} -> {"auth"} -> valid_user($args -> {"username"}, $args -> {"password"}); + + # If the user is valid, is the account active? + if($user) { + # If the account is active, the user is good to go (AuthMethods that don't support activation + # will return true here always, so there's no need to explicitly check activation support) + if($self -> {"session"} -> {"auth"} -> activated($args -> {"username"})) { + return ($user, $args); + + } else { + # Otherwise, send back the 'account needs activating' error + return ($self -> {"template"} -> load_template("login/error.tem", + {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_INACTIVE", + { "***url-resend***" => $self -> build_url("block" => "login", "pathinfo" => [ "resend" ]) }) + }), $args); + } + } + + # Work out why the login failed (this may be an internal error, or a fallback) + my $failmsg = $self -> {"session"} -> auth_error() || $self -> {"template"} -> replace_langvar("LOGIN_ERR_INVALID"); + + # Try marking the login failure. If username is not valid, this will return undefs + my ($failcount, $limit) = $self -> {"session"} -> {"auth"} -> mark_loginfail($args -> {"username"}); + + # Is login failure limiting even supported? + if(defined($failcount) && defined($limit) && ($limit > 0)) { + # Is the user within the login limit? + if($failcount <= $limit) { + # Yes, return a fail message, potentially with a failure limit warning + return ($self -> {"template"} -> load_template("login/failed.tem", + {"***reason***" => $failmsg, + "***failcount***" => $failcount, + "***faillimit***" => $limit, + "***failremain***" => $limit - $failcount, + "***url-recover***" => $self -> build_url("block" => "login", "pathinfo" => [ "recover" ]) + }), $args); + + # User has exceeded failure limit but their account is still active, deactivate + # their account, send an email, and return an appropriate error + } elsif($self -> {"session"} -> {"auth"} -> activated($args -> {"username"})) { + + # Get the user data - the user must exist to get past the defined() guards above, but check anyway + my $user = $self -> {"session"} -> {"auth"} -> get_user($args -> {"username"}); + if($user) { + my ($newpass, $actcode) = $self -> {"session"} -> {"auth"} -> reset_password_actcode($args -> {"username"}); + + $self -> lockout_email($user, $newpass, $actcode, $limit); + + return ($self -> {"template"} -> load_template("login/lockedout.tem", + {"***reason***" => $failmsg, + "***failcount***" => $failcount, + "***faillimit***" => $limit, + "***failremain***" => $limit - $failcount + }), $args); + } + } + } + + # limiting not supported, or username is bunk - return the failure message as-is + return ($self -> {"template"} -> load_template("login/error.tem", + { + "***reason***" => $failmsg + }), $args); +} + + +## @method private @ validate_passchange() +# Determine whether the password change request made by the user is valid. If the +# password change is valid (passwords match, pass policy, and the old password is +# valid), this will change the password for the user before returning. +# +# @return An array of two values: A reference to the user's data on success, +# or an error string if the change failed, and a reference to a hash of +# arguments that passed validation. +sub validate_passchange { + my $self = shift; + my $error = ""; + my $errors = ""; + my $args = {}; + + # Need to get a logged-in user before anything else is done + my $user = $self -> {"session"} -> get_user_byid(); + return ($self -> {"template"} -> load_template("login/passchange_errors.tem", + {"***errors***" => $self -> {"template"} -> load_template("login/passchange_error.tem", + {"***error***" => "{L_LOGIN_PASSCHANGE_ERRNOUSER}"}) + }), $args) + if($self -> {"session"} -> anonymous_session() || !$user); + + # Double-check that the user's authmethod actually /allows/ password changes + my $auth_passchange = $self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "passchange"); + return ($self -> {"template"} -> load_template("login/passchange_errors.tem", + {"***errors***" => $self -> {"template"} -> load_template("login/passchange_error.tem", + {"***error***" => $self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "passchange_message")}) + }), $args) + if(!$auth_passchange); + + # Got a user, so pull in the passwords - new, confirm, and old. + ($args -> {"newpass"}, $error) = $self -> validate_string("newpass", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_NEWPASSWORD"), + "minlen" => 2, + "maxlen" => 255}); + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => $error}) + if($error); + + ($args -> {"confirm"}, $error) = $self -> validate_string("confirm", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_CONFPASS"), + "minlen" => 2, + "maxlen" => 255}); + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => $error}) + if($error); + + # New and confirm must match + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => "{L_LOGIN_PASSCHANGE_ERRMATCH}"}) + unless($args -> {"newpass"} eq $args -> {"confirm"}); + + ($args -> {"oldpass"}, $error) = $self -> validate_string("oldpass", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_OLDPASS"), + "minlen" => 2, + "maxlen" => 255}); + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => $error}) + if($error); + + # New and old must not match + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => "{L_LOGIN_PASSCHANGE_ERRSAME}"}) + if($args -> {"newpass"} eq $args -> {"oldpass"}); + + # Check that the old password is actually valid + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => "{L_LOGIN_PASSCHANGE_ERRVALID}"}) + unless($self -> {"session"} -> {"auth"} -> valid_user($user -> {"username"}, $args -> {"oldpass"})); + + # Now apply policy if needed + my $policy_fails = $self -> {"session"} -> {"auth"} -> apply_policy($user -> {"username"}, $args -> {"newpass"}); + + if($policy_fails) { + foreach my $name (@{$policy_fails -> {"policy_order"}}) { + next if(!$policy_fails -> {$name}); + $errors .= $self -> {"template"} -> load_template("login/passchange_error.tem", {"***error***" => "{L_LOGIN_".uc($name)."ERR}", + "***set***" => $policy_fails -> {$name} -> [1], + "***require***" => $policy_fails -> {$name} -> [0] }); + } + } + + # Any errors accumulated up to this point mean that changes don't happen... + return ($self -> {"template"} -> load_template("login/passchange_errors.tem", {"***errors***" => $errors}), $args) + if($errors); + + # Password is good, change it + $self -> {"session"} -> {"auth"} -> set_password($user -> {"username"}, $args -> {"newpass"}) + or return ($self -> {"template"} -> load_template("login/passchange_errors.tem", + {"***errors***" => $self -> {"template"} -> load_template("login/passchange_error.tem", + {"***error***" => $self -> {"session"} -> {"auth"} -> errstr()}) + }), $args); + + # No need to keep the passchange variable now + $self -> {"session"} -> set_variable("passchange_reason", undef); + + return ($user, $args); +} + + +## @method private @ validate_register() +# Determine whether the username, email, and security question provided by the user +# are valid. If they are, return true. +# +# @return The new user's record on success, an error string if the register failed. +sub validate_register { + my $self = shift; + my $error = ""; + my $errors = ""; + my $args = {}; + + # User attempted self-register when it is disabled? Naughty user, no cookie! + return ($self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_NOSELFREG")}), $args) + unless($self -> {"settings"} -> {"config"} -> {"Login:allow_self_register"}); + + # Check that the username is provided and valid + ($args -> {"regname"}, $error) = $self -> validate_string("regname", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_USERNAME"), + "minlen" => 2, + "maxlen" => 32, + "formattest" => '^[-\w]+$', + "formatdesc" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADUSERCHAR") + }); + # Is the username valid? + if($error) { + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $error}); + } else { + # Is the username in use? + my $user = $self -> {"session"} -> get_user($args -> {"regname"}); + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", + {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_USERINUSE", + { "***url-recover***" => $self -> build_url("block" => "login", "pathinfo" => [ "recover" ]) }) + }) + if($user); + } + + # And the email + ($args -> {"email"}, $error) = $self -> validate_string("email", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_EMAIL"), + "minlen" => 2, + "maxlen" => 256 + }); + if($error) { + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $error}); + } else { + + # Check that the address is structured in a vaguely valid way + # Yes, this is not fully RFC compliant, but frankly going down that road invites a + # level of utter madness that would make Azathoth himself utter "I say, steady on now..." + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADEMAIL")}) + if($args -> {"email"} !~ /^[\w.+-]+\@([\w-]+\.)+\w+$/); + + # Is the email address in use? + my $user = $self -> {"session"} -> {"auth"} -> {"app"} -> get_user_byemail($args -> {"email"}); + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_EMAILINUSE", + { "***url-recover***" => $self -> build_url("block" => "login", "pathinfo" => [ "recover" ]) }) + }) + if($user); + } + + # Did the user get the 'Are you a human' question right? + ($args -> {"answer"}, $error) = $self -> validate_string("answer", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_SECURITY"), + "minlen" => 2, + "maxlen" => 255, + }); + if($error) { + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $error}); + } else { + $errors .= $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADSECURE")}) + unless(lc($args -> {"answer"}) eq lc($self -> {"settings"} -> {"config"} -> {"Login:self_register_answer"})); + } + + # Halt here if there are any problems. + return ($self -> {"template"} -> load_template("login/reg_errorlist.tem", {"***errors***" => $errors}), $args) + if($errors); + + # Get here an the user's details are okay, register the new user. + my $methodimpl = $self -> {"session"} -> {"auth"} -> get_authmethod_module($self -> {"settings"} -> {"config"} -> {"default_authmethod"}) + or return ($self -> {"template"} -> load_template("login/reg_errorlist.tem", + {"***errors***" => $self -> {"template"} -> load_template("login/reg_error.tem", + {"***reason***" => $self -> {"session"} -> {"auth"} -> errstr()}) }), + $args); + + my ($user, $password) = $methodimpl -> create_user($args -> {"regname"}, $self -> {"settings"} -> {"config"} -> {"default_authmethod"}, $args -> {"email"}); + return ($self -> {"template"} -> load_template("login/reg_errorlist.tem", + {"***errors***" => $self -> {"template"} -> load_template("login/reg_error.tem", {"***reason***" => $methodimpl -> errstr()}) }), + $args) + if(!$user); + + # Send registration email + my $err = $self -> register_email($user, $password); + return ($err, $args) if($err); + + # User is registered... + return ($user, $args); +} + + +## @method private @ validate_actcode() +# Determine whether the activation code provided by the user is valid +# +# @return An array of two values: the first is a reference to the activated +# user's data hash on success, an error message otherwise; the +# second is the args parsed from the activation data. +sub validate_actcode { + my $self = shift; + my $args = {}; + my $error; + + # Check that the code has been provided and contains allowed characters + ($args -> {"actcode"}, $error) = $self -> validate_string("actcode", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_ACTCODE"), + "minlen" => 64, + "maxlen" => 64, + "formattest" => '^[a-zA-Z0-9]+$', + "formatdesc" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADACTCHAR")}); + # Bomb out at this point if the code is not valid. + return $self -> {"template"} -> load_template("login/act_error.tem", {"***reason***" => $error}) + if($error); + + # Act code is valid, can a user be activated? + # Note that this can not determine whether the user's auth method supports activation ahead of time, as + # we don't actually know which user is being activated until the actcode lookup is done. And generally, if + # an act code has been set, the authmethod supports activation anyway! + my $user = $self -> {"session"} -> {"auth"} -> activate_user($args -> {"actcode"}); + return ($self -> {"template"} -> load_template("login/act_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADCODE")}), $args) + unless($user); + + # User is active + return ($user, $args); +} + + +## @method private @ validate_resend() +# Determine whether the email address the user entered is valid, and whether the +# the account needs to be (or can be) activated. If it is, generate a new password +# and activation code to send to the user. +# +# @return Two values: a reference to the user whose activation code has been send +# on success, or an error message, and a reference to a hash containing +# the data entered by the user. +sub validate_resend { + my $self = shift; + my $args = {}; + my $error; + + # Get the email address entered by the user + ($args -> {"email"}, $error) = $self -> validate_string("email", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_RESENDEMAIL"), + "minlen" => 2, + "maxlen" => 256 + }); + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $error}), $args) + if($error); + + # Does the email look remotely valid? + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADEMAIL")}), $args) + if($args -> {"email"} !~ /^[\w.+-]+\@([\w-]+\.)+\w+$/); + + # Does the address correspond to an actual user? + my $user = $self -> {"session"} -> {"auth"} -> {"app"} -> get_user_byemail($args -> {"email"}); + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADUSER")}), $args) + if(!$user); + + # Does the user's authmethod support activation anyway? + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "activate_message")}), $args) + if(!$self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "activate")); + + # no point in resending an activation code to an active account + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_ALREADYACT")}), $args) + if($self -> {"session"} -> {"auth"} -> activated($user -> {"username"})); + + my $newpass; + ($newpass, $user -> {"act_code"}) = $self -> {"session"} -> {"auth"} -> reset_password_actcode($user -> {"username"}); + return ($self -> {"template"} -> load_template("login/resend_error.tem", {"***reason***" => $self -> {"session"} -> {"auth"} -> {"app"} -> errstr()}), $args) + if(!$newpass); + + # Get here and the user's account isn't active, needs to be activated, and can be emailed a code... + $self -> resend_act_email($user, $newpass); + + return($user, $args); +} + + +## @method private @ validate_recover() +# Determine whether the email address the user entered is valid, and if so generate +# an act code to start the reset process. +# +# @return Two values: a reference to the user whose reset code has been send +# on success, or an error message, and a reference to a hash containing +# the data entered by the user. +sub validate_recover { + my $self = shift; + my $args = {}; + my $error; + + # Get the email address entered by the user + ($args -> {"email"}, $error) = $self -> validate_string("email", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_RECOVER_EMAIL"), + "minlen" => 2, + "maxlen" => 256 + }); + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $error}), $args) + if($error); + + # Does the email look remotely valid? + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADEMAIL")}), $args) + if($args -> {"email"} !~ /^[\w.+-]+\@([\w-]+\.)+\w+$/); + + # Does the address correspond to an actual user? + my $user = $self -> {"session"} -> {"auth"} -> {"app"} -> get_user_byemail($args -> {"email"}); + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADUSER")}), $args) + if(!$user); + + # Users can not recover an inactive account - they need to get a new act code + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_NORECINACT")}), $args) + if($self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "activate") && + !$self -> {"session"} -> {"auth"} -> activated($user -> {"username"})); + + # Does the user's authmethod support activation anyway? + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "recover_message")}), $args) + if(!$self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "recover")); + + my $newcode = $self -> {"session"} -> {"auth"} -> generate_actcode($user -> {"username"}); + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"session"} -> {"auth"} -> {"app"} -> errstr()}), $args) + if(!$newcode); + + # Get here and the user's account has been reset + $self -> recover_email($user, $newcode); + + return($user, $args); +} + + +## @method private @ validate_reset() +# Pull the userid and activation code out of the submitted data, and determine +# whether they are valid (and that the user's authmethod allows for resets). If +# so, reset the user's password and send and email to them with the new details. +# +# @return Two values: a reference to the user whose password has been reset +# on success, or an error message, and a reference to a hash containing +# the data entered by the user. +sub validate_reset { + my $self = shift; + my $args = {}; + my $error; + + # Obtain the userid from the query string, if possible. + my $uid = is_defined_numeric($self -> {"cgi"}, "uid") + or return ($self -> {"template"} -> replace_langvar("LOGIN_ERR_NOUID"), $args); + + my $user = $self -> {"session"} -> {"auth"} -> {"app"} -> get_user_byid($uid) + or return ($self -> {"template"} -> replace_langvar("LOGIN_ERR_BADUID"), $args); + + # Get the reset code, should be a 64 character alphanumeric string + ($args -> {"resetcode"}, $error) = $self -> validate_string("resetcode", {"required" => 1, + "nicename" => $self -> {"template"} -> replace_langvar("LOGIN_RESETCODE"), + "minlen" => 64, + "maxlen" => 64, + "formattest" => '^[a-zA-Z0-9]+$', + "formatdesc" => $self -> {"template"} -> replace_langvar("LOGIN_ERR_BADRECCHAR")}); + return ($error, $args) if($error); + + # Does the reset code match the one set for the user? + return ($self -> {"template"} -> replace_langvar("LOGIN_ERR_BADRECCODE"), $args) + unless($user -> {"act_code"} && $user -> {"act_code"} eq $args -> {"resetcode"}); + + # Users can not recover an inactive account - they need to get a new act code + return ($self -> {"template"} -> replace_langvar("LOGIN_ERR_NORECINACT"), $args) + if($self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "activate") && + !$self -> {"session"} -> {"auth"} -> activated($user -> {"username"})); + + # double-check the authmethod supports resets, just to be on the safe side (the code should never + # get here if it does not, but better safe than sorry) + return ($self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "recover_message"), $args) + if(!$self -> {"session"} -> {"auth"} -> capabilities($user -> {"username"}, "recover")); + + # Okay, user is valid, authcode checks out, auth module supports resets, generate a new + # password and send it + my $newpass = $self -> {"session"} -> {"auth"} -> reset_password($user -> {"username"}); + return ($self -> {"template"} -> load_template("login/recover_error.tem", {"***reason***" => $self -> {"session"} -> {"auth"} -> errstr()}), $args) + if(!$newpass); + + # Get here and the user's account has been reset + $self -> reset_email($user, $newpass); + + return($user, $args); +} + + +# ============================================================================ +# Form generators + +## @method private $ generate_login_form($error, $args) +# Generate the content of the login form. +# +# @param error A string containing errors related to logging in, or undef. +# @param args A reference to a hash of intiial values. +# @return An array of two values: the page title, and a string containing +# the login form. +sub generate_login_form { + my $self = shift; + my $error = shift; + my $args = shift; + + # Wrap the error message in a message box if we have one. + $error = $self -> {"template"} -> load_template("login/error_box.tem", {"***message***" => $error}) + if($error); + + # Persist length is always in seconds, so convert it to something more readable + my $persist_length = $self -> {"template"} -> humanise_seconds($self -> {"session"} -> {"auth"} -> get_config("max_autologin_time")); + + # if self-registration is enabled, turn on the option + my $self_register = $self -> {"settings"} -> {"config"} -> {"Login:allow_self_register"} ? + $self -> {"template"} -> load_template("login/selfreg.tem") : + $self -> {"template"} -> load_template("login/no_selfreg.tem"); + + return ($self -> {"template"} -> replace_langvar("LOGIN_TITLE"), + $self -> {"template"} -> load_template("login/form.tem", {"***error***" => $error, + "***persistlen***" => $persist_length, + "***selfreg***" => $self_register, + "***url-actform***" => $self -> build_url("block" => "login", "pathinfo" => [ "activate" ]), + "***url-recform***" => $self -> build_url("block" => "login", "pathinfo" => [ "recover" ]), + "***target***" => $self -> build_url("block" => "login"), + "***question***" => $self -> {"settings"} -> {"config"} -> {"Login:self_register_question"}, + "***username***" => $args -> {"username"}, + "***regname***" => $args -> {"regname"}, + "***email***" => $args -> {"email"}})); +} + + +## @method private @ generate_passchange_form($error) +# Generate a form through which the user can change their password, used to +# support forced password changes. +# +# @param error A string containing errors related to password changes, or undef. +# @return An array of two values: the page title string, the code form +sub generate_passchange_form { + my $self = shift; + my $error = shift; + my $reasons = { 'temporary' => "{L_LOGIN_FORCECHANGE_TEMP}", + 'expired' => "{L_LOGIN_FORCECHANGE_OLD}", }; + + # convert the password policy to a string + my $policy = $self -> build_password_policy("login/policy.tem") || "{L_LOGIN_POLICY_NONE}"; + + # Reason should be in the 'passchange_reason' session variable. + my $reason = $self -> {"session"} -> get_variable("passchange_reason", "temporary"); + + # Force a sane reason + $reason = 'temporary' unless($reason && $reasons -> {$reason}); + + # Wrap the error message in a message box if we have one. + $error = $self -> {"template"} -> load_template("login/error_box.tem", {"***message***" => $error}) + if($error); + + return ($self -> {"template"} -> replace_langvar("LOGIN_TITLE"), + $self -> {"template"} -> load_template("login/force_password.tem", {"***error***" => $error, + "***target***" => $self -> build_url("block" => "login"), + "***policy***" => $policy, + "***reason***" => $reasons -> {$reason}, + "***rid***" => $reason } )); +} + + + +## @method private @ generate_actcode_form($error) +# Generate a form through which the user may specify an activation code. +# +# @param error A string containing errors related to activating, or undef. +# @return An array of two values: the page title string, the code form +sub generate_actcode_form { + my $self = shift; + my $error = shift; + + # Wrap the error message in a message box if we have one. + $error = $self -> {"template"} -> load_template("login/error_box.tem", {"***message***" => $error}) + if($error); + + return ($self -> {"template"} -> replace_langvar("LOGIN_TITLE"), + $self -> {"template"} -> load_template("login/act_form.tem", {"***error***" => $error, + "***target***" => $self -> build_url("block" => "login"), + "***url-resend***" => $self -> build_url("block" => "login", "pathinfo" => [ "resend" ]),})); +} + + +## @method private @ generate_recover_form($error) +# Generate a form through which the user may recover their account details. +# +# @param error A string containing errors related to recovery, or undef. +# @return An array of two values: the page title string, the code form +sub generate_recover_form { + my $self = shift; + my $error = shift; + + # Wrap the error message in a message box if we have one. + $error = $self -> {"template"} -> load_template("login/error_box.tem", {"***message***" => $error}) + if($error); + + return ($self -> {"template"} -> replace_langvar("LOGIN_TITLE"), + $self -> {"template"} -> load_template("login/recover_form.tem", {"***error***" => $error, + "***target***" => $self -> build_url("block" => "login")})); +} + + +## @method private @ generate_resend_form($error) +# Generate a form through which the user may resend their account activation code. +# +# @param error A string containing errors related to resending, or undef. +# @return An array of two values: the page title string, the code form +sub generate_resend_form { + my $self = shift; + my $error = shift; + + # Wrap the error message in a message box if we have one. + $error = $self -> {"template"} -> load_template("login/error_box.tem", {"***message***" => $error}) + if($error); + + return ($self -> {"template"} -> replace_langvar("LOGIN_TITLE"), + $self -> {"template"} -> load_template("login/resend_form.tem", {"***error***" => $error, + "***target***" => $self -> build_url("block" => "login")})); +} + + +# ============================================================================ +# Response generators + +## @method private @ generate_loggedin() +# Generate the contents of a page telling the user that they have successfully logged in. +# +# @return An array of three values: the page title string, the 'logged in' message, and +# a meta element to insert into the head element to redirect the user. +sub generate_loggedin { + my $self = shift; + + my $url = $self -> build_return_url(); + my $warning = ""; + + # The user validation might have thrown up warning, so check that. + $warning = $self -> {"template"} -> load_template("login/warning_box.tem", {"***message***" => $self -> {"session"} -> auth_error()}) + if($self -> {"session"} -> auth_error()); + + my ($content, $extrahead); + + # If any warnings were encountered, send back a different logged-in page to avoid + # confusing users. + if(!$warning) { + # Note that, while it would be nice to immediately redirect users at this point, + $content = $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_LONGDESC", {"***url***" => $url}), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("SITE_CONTINUE"), + "colour" => "blue", + "action" => "location.href='$url'"} ]); + $extrahead = $self -> {"template"} -> load_template("refreshmeta.tem", {"***url***" => $url}); + + # Users who have encountered warnings during login always get a login confirmation page, as it has + # to show them the warning message box. + } else { + my $message = $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_NOREDIRECT", {"***url***" => $url, + "***supportaddr***" => ""}), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("SITE_CONTINUE"), + "colour" => "blue", + "action" => "location.href='$url'"} ]); + $content = $self -> {"template"} -> load_template("login/login_warn.tem", {"***message***" => $message, + "***warning***" => $warning}); + } + + # return the title, content, and extraheader. If the warning is set, do not include an autoredirect. + return ($self -> {"template"} -> replace_langvar("LOGIN_DONETITLE"), + $content, + $extrahead); +} + + +## @method private @ generate_loggedout() +# Generate the contents of a page telling the user that they have successfully logged out. +# +# @return An array of three values: the page title string, the 'logged out' message, and +# a meta element to insert into the head element to redirect the user. +sub generate_loggedout { + my $self = shift; + + # NOTE: This is called **after** the session is deleted, so savestate will be undef. This + # means that the user will be returned to a default (the login form, usually). + my $url = $self -> build_return_url(); + + # return the title, content, and extraheader + return ($self -> {"template"} -> replace_langvar("LOGOUT_TITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGOUT_TITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGOUT_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGOUT_LONGDESC", {"***url***" => $url}), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("SITE_CONTINUE"), + "colour" => "blue", + "action" => "location.href='$url'"} ]), + $self -> {"template"} -> load_template("refreshmeta.tem", {"***url***" => $url})); +} + + +## @method private @ generate_activated($user) +# Generate the contents of a page telling the user that they have successfully activated +# their account. +# +# @return An array of two values: the page title string, the 'activated' message. +sub generate_activated { + my $self = shift; + + my $target = $self -> build_url(block => "login", + pathinfo => []); + + return ($self -> {"template"} -> replace_langvar("LOGIN_ACT_DONETITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_ACT_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_ACT_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_ACT_LONGDESC", + {"***url-login***" => $self -> build_url("block" => "login")}), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_LOGIN"), + "colour" => "blue", + "action" => "location.href='$target'"} ])); +} + + +## @method private @ generate_registered() +# Generate the contents of a page telling the user that they have successfully created an +# inactive account. +# +# @return An array of two values: the page title string, the 'registered' message. +sub generate_registered { + my $self = shift; + + my $url = $self -> build_url(block => "login", + pathinfo => [ "activate" ]); + + return ($self -> {"template"} -> replace_langvar("LOGIN_REG_DONETITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_REG_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_REG_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_REG_LONGDESC"), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_ACTIVATE"), + "colour" => "blue", + "action" => "location.href='$url'"} ])); +} + + +## @method private @ generate_resent() +# Generate the contents of a page telling the user that a new activation code has been +# sent to their email address. +# +# @return An array of two values: the page title string, the 'resent' message. +sub generate_resent { + my $self = shift; + + my $url = $self -> build_url("block" => "login", "pathinfo" => [ "activate" ]); + + return ($self -> {"template"} -> replace_langvar("LOGIN_RESEND_DONETITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_RESEND_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_RESEND_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_RESEND_LONGDESC"), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_ACTIVATE"), + "colour" => "blue", + "action" => "location.href='$url'"} ])); +} + + +## @method private @ generate_recover() +# Generate the contents of a page telling the user that a new password has been +# sent to their email address. +# +# @return An array of two values: the page title string, the 'recover sent' message. +sub generate_recover { + my $self = shift; + + my $url = $self -> build_url("block" => "login", "pathinfo" => []); + + return ($self -> {"template"} -> replace_langvar("LOGIN_RECOVER_DONETITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_RECOVER_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_RECOVER_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_RECOVER_LONGDESC"), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_LOGIN"), + "colour" => "blue", + "action" => "location.href='$url'"} ])); +} + + +## @method private @ generate_reset() +# Generate the contents of a page telling the user that a new password has been +# sent to their email address. +# +# @param error If set, display an error message rather than a 'completed' message. +# @return An array of two values: the page title string, the 'resent' message. +sub generate_reset { + my $self = shift; + my $error = shift; + + my $url = $self -> build_url("block" => "login", "pathinfo" => []); + + if(!$error) { + return ($self -> {"template"} -> replace_langvar("LOGIN_RESET_DONETITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_RESET_DONETITLE"), + "security", + $self -> {"template"} -> replace_langvar("LOGIN_RESET_SUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_RESET_LONGDESC"), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_LOGIN"), + "colour" => "blue", + "action" => "location.href='$url'"} ])); + } else { + return ($self -> {"template"} -> replace_langvar("LOGIN_RESET_ERRTITLE"), + $self -> {"template"} -> message_box($self -> {"template"} -> replace_langvar("LOGIN_RESET_ERRTITLE"), + "error", + $self -> {"template"} -> replace_langvar("LOGIN_RESET_ERRSUMMARY"), + $self -> {"template"} -> replace_langvar("LOGIN_RESET_ERRDESC", {"***reason***" => $error}), + undef, + "logincore", + [ {"message" => $self -> {"template"} -> replace_langvar("LOGIN_LOGIN"), + "colour" => "blue", + "action" => "location.href='$url'"} ])); + } +} + + +# ============================================================================ +# API handling + + +## @method private $ _build_login_check_response(void) +# Determine whether the user's session is still login +# +# @return The data to send back to the user in an API response. +sub _build_login_check_response { + my $self = shift; + + return { "login" => {"loggedin" => $self -> {"session"} -> anonymous_session() ? "no" : "yes" }}; +} + + +## @method private $ _build_loginform_response(void) +# Generate the HTML to send back in response to a loginform API request. +# +# @return The HTML to send back to the client. +sub _build_loginform_response { + my $self = shift; + + return $self -> {"template"} -> load_template("login/apiform.tem"); +} + + +sub _build_login_response { + my $self = shift; + + my ($user, $args) = $self -> validate_login(); + if(ref($user) eq "HASH") { + $self -> {"session"} -> create_session($user -> {"user_id"}); + $self -> log("login", $user -> {"username"}); + + my $cookies = $self -> {"session"} -> session_cookies(); + + return { "login" => { "loggedin" => "yes", + "user" => $user -> {"user_id"}, + "sid" => $self -> {"session"} -> {"sessid"}, + "cookies" => $cookies}}; + } else { + return { "login" => { "loggedin" => "no", + "content" => $user}}; + } +} + + +# ============================================================================ +# Interface functions + +## @method $ page_display() +# Generate the page content for this module. +sub page_display { + my $self = shift; + + # Is this an API call, or a normal page operation? + my $apiop = $self -> is_api_operation(); + if(defined($apiop)) { + # API call - dispatch to appropriate handler. + given($apiop) { + when("check") { return $self -> api_response ($self -> _build_login_check_response()); } + when("loginform") { return $self -> api_html_response($self -> _build_loginform_response()); } + when("login") { return $self -> api_response ($self -> _build_login_response()); } + + default { + return $self -> api_response($self -> api_errorhash('bad_op', + $self -> {"template"} -> replace_langvar("API_BAD_OP"))) + } + } + } else { + # We need to determine what the page title should be, and the content to shove in it... + my ($title, $body, $extrahead) = ("", "", ""); + my @pathinfo = $self -> {"cgi"} -> multi_param("pathinfo"); + + # User is attempting to do a password change + if(defined($self -> {"cgi"} -> param("changepass"))) { + + # Check the password is valid + my ($user, $args) = $self -> validate_passchange(); + + # Change failed, send back the change form + if(!ref($user)) { + $self -> log("passchange error", $user); + ($title, $body) = $self -> generate_passchange_form($user); + + # Change done, send back the loggedin page + } else { + $self -> log("password updated", $user); + ($title, $body, $extrahead) = $self -> generate_loggedin(); + } + + # If the user is not anonymous, they have logged in already. + } elsif(!$self -> {"session"} -> anonymous_session()) { + + # Is the user requesting a logout? If so, doo eet. + if(defined($self -> {"cgi"} -> param("logout")) || ($pathinfo[0] && $pathinfo[0] eq "logout")) { + $self -> log("logout", $self -> {"session"} -> get_session_userid()); + if($self -> {"session"} -> delete_session()) { + ($title, $body, $extrahead) = $self -> generate_loggedout(); + } else { + return $self -> generate_fatal($SessionHandler::errstr); + } + + # Already logged in, check password and either force a change or tell the user they logged in. + } else { + my $user = $self -> {"session"} -> get_user_byid(); + + if($user) { + # Does the user need to change their password? + my $passchange = $self -> {"session"} -> {"auth"} -> force_passchange($user -> {"username"}); + if(!$passchange) { + $self -> log("login", "Revisit to login form by logged in user ".$user -> {"username"}); + + # No passchange needed, user is good + ($title, $body, $extrahead) = $self -> generate_loggedin(); + } else { + $self -> {"session"} -> set_variable("passchange_reason", $passchange); + ($title, $body) = $self -> generate_passchange_form(); + } + } else { + $self -> {"logger"} -> die_log($self -> {"cgi"} -> remote_host(), "Logged in session with no user record. This Should Not Happen."); + } + } + + # User is anonymous - do we have a login? + } elsif(defined($self -> {"cgi"} -> param("login"))) { + + # Validate the other fields... + my ($user, $args) = $self -> validate_login(); + + # Do we have any errors? If so, send back the login form with them + if(!ref($user)) { + $self -> log("login error", $user); + ($title, $body) = $self -> generate_login_form($user, $args); + + # No errors, user is valid... + } else { + # should the login be made persistent? + my $persist = defined($self -> {"cgi"} -> param("persist")) && $self -> {"cgi"} -> param("persist"); + + # Get the session variables so they can be copied to the new session. + my ($block, $pathinfo, $api, $qstring) = $self -> get_saved_state(); + + # create the new logged-in session, copying over the savestate session variable + $self -> {"session"} -> create_session($user -> {"user_id"}, + $persist, + {"saved_block" => $block, + "saved_pathinfo" => $pathinfo, + "saved_api" => $api, + "saved_qstring" => $qstring}); + + $self -> log("login", $user -> {"username"}); + + # Does the user need to change their password? + my $passchange = $self -> {"session"} -> {"auth"} -> force_passchange($args -> {"username"}); + if(!$passchange) { + # No passchange needed, user is good + ($title, $body, $extrahead) = $self -> generate_loggedin(); + } else { + $self -> {"session"} -> set_variable("passchange_reason", $passchange); + ($title, $body) = $self -> generate_passchange_form(); + } + } + + # Has a registration attempt been made? + } elsif(defined($self -> {"cgi"} -> param("register"))) { + + # Validate/perform the registration + my ($user, $args) = $self -> validate_register(); + + # Do we have any errors? If so, send back the login form with them + if(!ref($user)) { + $self -> log("registration error", $user); + ($title, $body) = $self -> generate_login_form($user, $args); + + # No errors, user is registered + } else { + # Do not create a new session - the user needs to confirm the account. + $self -> log("registered inactive", $user -> {"username"}); + ($title, $body) = $self -> generate_registered(); + } + + # Is the user attempting activation? + } elsif(defined($self -> {"cgi"} -> param("actcode"))) { + + my ($user, $args) = $self -> validate_actcode(); + if(!ref($user)) { + $self -> log("activation error", $user); + ($title, $body) = $self -> generate_actcode_form($user); + } else { + $self -> log("activation success", $user -> {"username"}); + ($title, $body) = $self -> generate_activated($user); + } + + # Password reset requested? + } elsif(defined($self -> {"cgi"} -> param("dorecover"))) { + + my ($user, $args) = $self -> validate_recover(); + if(!ref($user)) { + $self -> log("Reset error", $user); + ($title, $body) = $self -> generate_recover_form($user); + } else { + $self -> log("Reset success", $user -> {"username"}); + ($title, $body) = $self -> generate_recover($user); + } + + } elsif(defined($self -> {"cgi"} -> param("resetcode"))) { + + my ($user, $args) = $self -> validate_reset(); + ($title, $body) = $self -> generate_reset(!ref($user) ? $user : undef); + # User wants a resend? + } elsif(defined($self -> {"cgi"} -> param("doresend"))) { + + my ($user, $args) = $self -> validate_resend(); + if(!ref($user)) { + $self -> log("Resend error", $user); + ($title, $body) = $self -> generate_resend_form($user); + } else { + $self -> log("Resend success", $user -> {"username"}); + ($title, $body) = $self -> generate_resent($user); + } + + + } elsif(defined($self -> {"cgi"} -> param("activate")) || ($pathinfo[0] && $pathinfo[0] eq "activate")) { + ($title, $body) = $self -> generate_actcode_form(); + + } elsif(defined($self -> {"cgi"} -> param("recover")) || ($pathinfo[0] && $pathinfo[0] eq "recover")) { + ($title, $body) = $self -> generate_recover_form(); + + } elsif(defined($self -> {"cgi"} -> param("resend")) || ($pathinfo[0] && $pathinfo[0] eq "resend")) { + ($title, $body) = $self -> generate_resend_form(); + + # No session, no submission? Send back the login form... + } else { + ($title, $body) = $self -> generate_login_form(); + } + + # Done generating the page content, return the filled in page template + return $self -> {"template"} -> load_template("login/page.tem", {"***title***" => $title, + "***extrahead***" => $extrahead, + "***content***" => $body,}); + } +} + +1; diff --git a/blocks/ORB/Userbar.pm b/blocks/ORB/Userbar.pm new file mode 100644 index 0000000..d94c25d --- /dev/null +++ b/blocks/ORB/Userbar.pm @@ -0,0 +1,122 @@ +## @file +# This file contains the implementation of the ORB user toolbar. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class ORB::Userbar +# The Userbar class encapsulates the code required to generate and +# manage the user toolbar. +package ORB::Userbar; + +use strict; +use parent qw(ORB); +use v5.12; + + +# ============================================================================== +# Bar generation + +## @method $ block_display($title, $current, $doclink) +# Generate a user toolbar, populating it as needed to reflect the user's options +# at the current time. +# +# @param title A string to show as the page title. +# @param current The current page name. +# @param doclink The name of a document link to include in the userbar. If not +# supplied, no link is shown. +# @return A string containing the user toolbar html on success, undef on error. +sub block_display { + my $self = shift; + my $title = shift; + my $current = shift; + my $doclink = shift; + + $self -> clear_error(); + + my $loginurl = $self -> build_url(block => "login", + fullurl => 1, + pathinfo => [], + params => {}, + forcessl => 1); + + my $fronturl = $self -> build_url(block => $self -> {"settings"} -> {"config"} -> {"default_block"}, + fullurl => 1, + pathinfo => [], + params => {}); + + # Initialise fragments to sane "logged out" defaults. + my ($import, $userprofile, $docs) = + ($self -> {"template"} -> load_template("userbar/import_disabled.tem"), + $self -> {"template"} -> load_template("userbar/profile_loggedout_http".($ENV{"HTTPS"} eq "on" ? "s" : "").".tem", {"***url-login***" => $loginurl}), + $self -> {"template"} -> load_template("userbar/doclink_disabled.tem"), + ); + + # Is documentation available? + my $url = $self -> get_documentation_url($doclink); + $docs = $self -> {"template"} -> load_template("userbar/doclink_enabled.tem", {"***url-doclink***" => $url}) + if($url); + + # Is the user logged in? + if(!$self -> {"session"} -> anonymous_session()) { + my $user = $self -> {"session"} -> get_user_byid() + or return $self -> self_error("Unable to obtain user data for logged in user. This should not happen!"); + + $import = $self -> {"template"} -> load_template("userbar/import_enabled.tem" , {"***url-import***" => $self -> build_url(block => "import", pathinfo => [])}) + if($self -> check_permission("import") && $current ne "import"); + + # User is logged in, so actually reflect their current options and state + $userprofile = $self -> {"template"} -> load_template("userbar/profile_loggedin.tem", {"***realname***" => $user -> {"fullname"}, + "***username***" => $user -> {"username"}, + "***gravhash***" => $user -> {"gravatar_hash"}, + "***url-logout***" => $self -> build_url(block => "login" , pathinfo => ["logout"])}); + } # if(!$self -> {"session"} -> anonymous_session()) + + return $self -> {"template"} -> load_template("userbar/userbar.tem", {"***pagename***" => $title, + "***front_url***" => $fronturl, + "***import***" => $import, + "***doclink***" => $docs, + "***profile***" => $userprofile}); +} + + +## @method $ page_display() +# Produce the string containing this block's full page content. This is primarily provided for +# API operations that allow the user to change their profile and settings. +# +# @return The string containing this block's page content. +sub page_display { + my $self = shift; + my ($content, $extrahead, $title); + + if(!$self -> {"session"} -> anonymous_session()) { + my $user = $self -> {"session"} -> get_user_byid() + or return ''; + + my $apiop = $self -> is_api_operation(); + if(defined($apiop)) { + given($apiop) { + default { + return $self -> api_html_response($self -> api_errorhash('bad_op', + $self -> {"template"} -> replace_langvar("API_BAD_OP"))) + } + } + } + } + + return "

".$self -> {"template"} -> replace_langvar("BLOCK_PAGE_DISPLAY")."

"; +} + +1; diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..e51a06b --- /dev/null +++ b/index.cgi @@ -0,0 +1,52 @@ +#!/usr/bin/perl -w +# Note: above -w flag should be removed in production, as it will cause warnings in +# 3rd party modules to appear in the server error log + +use utf8; +use v5.12; +use lib qw(/var/www/webperl); +use FindBin; + +# Work out where the script is, so module and config loading can work. +my $scriptpath; +BEGIN { + if($FindBin::Bin =~ /(.*)/) { + $scriptpath = $1; + } +} + +use CGI::Carp qw(fatalsToBrowser set_message); # Catch as many fatals as possible and send them to the user as well as stderr + +use lib "$scriptpath/modules"; + +my $contact = 'contact@email.address'; # global contact address, for error messages + +# System modules +use CGI::Carp qw(fatalsToBrowser set_message); # Catch as many fatals as possible and send them to the user as well as stderr + +# Webperl modules +use Webperl::Application; + +# Webapp modules +use ORB::AppUser; +use ORB::BlockSelector; +use ORB::System; + +delete @ENV{qw(PATH IFS CDPATH ENV BASH_ENV)}; # Clean up ENV + +# install more useful error handling +sub handle_errors { + my $msg = shift; + print "

Software error

\n"; + print '

Server time: ',scalar(localtime()),'
Error was:

',$msg,'
'; + print '

Please report this error to ',$contact,' giving the text of this error and the time and date at which it occured

'; +} +set_message(\&handle_errors); + +do { + my $app = Webperl::Application -> new(appuser => ORB::AppUser -> new(), + system => ORB::System -> new(), + block_selector => ORB::BlockSelector -> new()) + or die "Unable to create application"; + $app -> run(); +} diff --git a/lang/en/aviary.lang b/lang/en/aviary.lang new file mode 100755 index 0000000..f0187cf --- /dev/null +++ b/lang/en/aviary.lang @@ -0,0 +1 @@ +AVIARY_TITLE = Aviary diff --git a/lang/en/calendar.lang b/lang/en/calendar.lang new file mode 100644 index 0000000..ea422e6 --- /dev/null +++ b/lang/en/calendar.lang @@ -0,0 +1,10 @@ +CALENDAR_TITLE = Tweet Schedule +CALENDAR_ADDTWEET = + Add tweet + +CALENDAR_DAY1 = Monday +CALENDAR_DAY2 = Tuesday +CALENDAR_DAY3 = Wednesday +CALENDAR_DAY4 = Thursday +CALENDAR_DAY5 = Friday +CALENDAR_DAY6 = Saturday +CALENDAR_DAY7 = Sunday diff --git a/lang/en/debug.lang b/lang/en/debug.lang new file mode 100755 index 0000000..dfe0a37 --- /dev/null +++ b/lang/en/debug.lang @@ -0,0 +1,5 @@ +DEBUG_TIMEUSED = Execution time +DEBUG_SECONDS = seconds +DEBUG_USER = User time +DEBUG_SYSTEM = System time +DEBUG_MEMORY = Memory used diff --git a/lang/en/global.lang b/lang/en/global.lang new file mode 100755 index 0000000..4e2e9cb --- /dev/null +++ b/lang/en/global.lang @@ -0,0 +1,55 @@ +EMAIL_SIG = The {V_[sitename]} Team + +SITE_CONTINUE = Continue + +API_BAD_OP = Unknown API operation requested. +API_BAD_CALL = Incorrect invocation of an API-only module. +API_ERROR = An API error has occurred: ***error*** + +API_SESSION_GONE = Your session appears to have timed out.

Click here to log in again + +# Times for Template::fancy_time +TIMES_JUSTNOW = just now +TIMES_SECONDS = %t seconds ago +TIMES_MINUTE = a minute ago +TIMES_MINUTES = %t minutes ago +TIMES_HOUR = an hour ago +TIMES_HOURS = %t hours ago +TIMES_DAY = a day ago +TIMES_DAYS = %t days ago +TIMES_WEEK = a week ago +TIMES_WEEKS = %t weeks ago +TIMES_MONTH = a month ago +TIMES_MONTHS = %t months ago +TIMES_YEAR = a year ago +TIMES_YEARS = %t years ago + +FUTURE_JUSTNOW = shortly +FUTURE_SECONDS = in %t seconds +FUTURE_MINUTE = in a minute +FUTURE_MINUTES = in %t minutes +FUTURE_HOUR = in an hour +FUTURE_HOURS = in %t hours +FUTURE_DAY = in a day +FUTURE_DAYS = in %t days +FUTURE_WEEK = in a week +FUTURE_WEEKS = in %t weeks +FUTURE_MONTH = in a month +FUTURE_MONTHS = in %t months +FUTURE_YEAR = in a year +FUTURE_YEARS = in %t years + +BLOCK_BLOCK_DISPLAY = Direct call to unimplemented block_display() +BLOCK_SECTION_DISPLAY = Direct call to unimplemented section_display() + +PAGE_ERROR = Error +PAGE_ERROROK = Okay + +FATAL_ERROR = Fatal Error +FATAL_ERROR_SUMMARY = The system has encountered a fatal error and can not continue. The error is shown below. + +CANCEL_OPTION = Cancel +CLOSE_OPTION = Close + +PAGE_POPUP = Error +PAGE_FOOTER = diff --git a/lang/en/import.lang b/lang/en/import.lang new file mode 100644 index 0000000..4774dc8 --- /dev/null +++ b/lang/en/import.lang @@ -0,0 +1,13 @@ +IMPORT_TITLE = Import Schedule + +IMPORT_INTRO = Use this form to import an Excel workbook (.xls format only, not .xlsx) into your schedule. Any previously imported scheduled messages that have not yet been posted will be removed as part of the import process. +IMPORT_EXCELFILE = Excel workbook file +IMPORT_SUBMIT = Import + +IMPORT_SUCCESS = Import completed successfully +IMPORT_SUMMARY = Import completed successfully +IMPORT_LONGDESC =

The schedule in the uploaded spreadsheet has been imported successfully. ***removed*** unposted scheduled messages were removed, and ***added*** messages were added.

+ +IMPORT_ERR_NOFILESET = No excel file selected, unable to import anything. +IMPORT_ERR_BADHANDLE = An internal file handle problem was encoutered. Please try again. +IMPORT_ERR_BADPARSER = Unable to create a spreadsheet parser object. diff --git a/lang/en/login.lang b/lang/en/login.lang new file mode 100755 index 0000000..be7ddb4 --- /dev/null +++ b/lang/en/login.lang @@ -0,0 +1,175 @@ +LOGIN_TITLE = Log in +LOGIN_LOGINFORM = Log in +LOGIN_INTRO = Enter your username and password to log in. +LOGIN_USERNAME = Username +LOGIN_PASSWORD = Password +LOGIN_EMAIL = Email address +LOGIN_PERSIST = Remember me +LOGIN_LOGIN = Log in +LOGIN_FAILED = Login failed +LOGIN_RECOVER = Forgotten your username or password? +LOGIN_SENDACT = Click to resend your activation code + +PERSIST_WARNING = WARNING: do not enable the "Remember me" option on shared, cluster, or public computers. This option should only be enabled on machines you have exclusive access to. + +LOGIN_DONETITLE = Logged in +LOGIN_SUMMARY = You have successfully logged into the system. +LOGIN_LONGDESC = You have successfully logged in, and you will be redirected shortly. If you do not want to wait, click continue. Alternatively, Click here to return to the front page. +LOGIN_NOREDIRECT = You have successfully logged in, but warnings were encountered during login. Please check the warning messages, and contact support if a serious problem has been encountered, otherwise, click continue. Alternatively, Click here to return to the front page. + +LOGOUT_TITLE = Logged out +LOGOUT_SUMMARY = You have successfully logged out. +LOGOUT_LONGDESC = You have successfully logged out, and you will be redirected shortly. If you do not want to wait, click continue. Alternatively, Click here to return to the front page. + +LOGIN_ERR_BADUSERCHAR = Illegal character in username. Usernames may only contain alphanumeric characters, underscores, or hyphens. +LOGIN_ERR_INVALID = Login failed: unknown username or password provided. + +# Registration-related stuff +LOGIN_REGISTER = Sign up +LOGIN_REG_INTRO = Create an account by choosing a username and giving a valid email address. A password will be emailed to you. +LOGIN_SECURITY = Security question +LOGIN_SEC_INTRO = In order to prevent abuse by automated spamming systems, please answer the following question to prove that you are a human.
Note: the answer is not case sensitive. +LOGIN_SEC_SUBMIT = Sign up + +LOGIN_ERR_NOSELFREG = Self-registration is not currently permitted. +LOGIN_ERR_REGFAILED = Registration failed +LOGIN_ERR_BADSECURE = You did not answer the security question correctly, please check your answer and try again. +LOGIN_ERR_BADEMAIL = The specified email address does not appear to be valid. +LOGIN_ERR_USERINUSE = The specified username is already in use. If you can't remember your password, please use the account recovery facility rather than attempt to make a new account. +LOGIN_ERR_EMAILINUSE = The specified email address is already in use. If you can't remember your username or password, please use the account recovery facility rather than attempt to make a new account. +LOGIN_ERR_INACTIVE = Your account is currently inactive. Please check your email for an 'Activation Required' email and follow the link it contains to activate your account. If you have not received an actication email, or need a new one, request a new activation email. + +# Registration done +LOGIN_REG_DONETITLE = Registration successful +LOGIN_REG_SUMMARY = Activation required! +LOGIN_REG_LONGDESC = A new user account has been created for you, and an email has been sent to you with your new account password and an activation link.

Please check your email for a message with the subject '{V_[sitename]} account created - Activation required!' and follow the instructions it contains to activate your account. + +# Registration email +LOGIN_REG_SUBJECT = {V_[sitename]} account created - Activation required! +LOGIN_REG_GREETING = Hi ***username*** +LOGIN_REG_CREATED = A new account in the {V_[sitename]} system has just been created for you. Your username and password for the system are given below. +LOGIN_REG_ACTNEEDED = Before you can log in, you must activate your account. To activate your account, please click on the following link, or copy and paste it into your web browser: +LOGIN_REG_ALTACT = Alternatively, enter the following code in the account activation form: +LOGIN_REG_ENJOY = Thank you for registering! + +# Activation related +LOGIN_ACTCODE = Activation code +LOGIN_ACTFAILED = User account activation failed +LOGIN_ACTFORM = Activate account +LOGIN_ACTINTRO = Please enter your 64 character activation code here. +LOGIN_ACTIVATE = Activate account +LOGIN_ERR_BADACTCHAR = Activation codes may only contain alphanumeric characters. +LOGIN_ERR_BADCODE = The provided activation code is invalid: either your account is already active, or you entered the code incorrectly. Note that the code is case sensitive - upper and lower case characters are treated differently. Please check you entered the code correctly. + +# Activation done +LOGIN_ACT_DONETITLE = Account activated +LOGIN_ACT_SUMMARY = Activation successful! +LOGIN_ACT_LONGDESC = Your new account has been acivated, and you can now log in using your username and the password emailed to you. + +# Recovery related +LOGIN_RECFORM = Recover account details +LOGIN_RECINTRO = If you have forgotten your username or password, enter the email address associated with your account in the field below. An email will be sent to you containing your username, and a link to click on to reset your password. If you do not have access to the email address associated with your account, please contact the site owner. +LOGIN_RECEMAIL = Email address +LOGIN_DORECOVER = Recover account +LOGIN_RECOVER_SUBJECT = Your {V_[sitename]} account +LOGIN_RECOVER_GREET = Hi ***username*** +LOGIN_RECOVER_INTRO = You, or someone pretending to be you, has requested that your password be reset. In order to reset your account, please click on the following link, or copy and paste it into your web browser. +LOGIN_RECOVER_IGNORE = If you did not request this reset, please either ignore this email or report it to the {V_[sitename]} administrator. +LOGIN_RECOVER_FAILED = Account recovery failed +LOGIN_RECOVER_DONETITLE = Account recovery code sent +LOGIN_RECOVER_SUMMARY = Recovery code sent! +LOGIN_RECOVER_LONGDESC = An account recovery code has been send to your email address.

Please check your email for a message with the subject 'Your {V_[sitename]} account' and follow the instructions it contains. +LOGIN_ERR_NOUID = No user id specified. +LOGIN_ERR_BADUID = The specfied user id is not valid. +LOGIN_ERR_BADRECCHAR = Account reset codes may only contain alphanumeric characters. +LOGIN_ERR_BADRECCODE = The provided account reset code is invalid. Note that the code is case sensitive - upper and lower case characters are treated differently. Please check you entered the code correctly. +LOGIN_ERR_NORECINACT = Your account is inactive, and therefore can not be recovered. In order to access your account, please request a new activation code and password. + +LOGIN_RESET_SUBJECT = Your {V_[sitename]} account +LOGIN_RESET_GREET = Hi ***username*** +LOGIN_RESET_INTRO = Your password has been reset, and your username and new password are given below: +LOGIN_RESET_LOGIN = To log into the {V_[sitename]}, please go to the following form and enter the username and password above. Once you have logged in, please change your password. + +LOGIN_RESET_DONETITLE = Account reset complete +LOGIN_RESET_SUMMARY = Password reset successfully +LOGIN_RESET_LONGDESC = Your username and a new password have been sent to your email address. Please look for an email with the subject 'Your {V_[sitename]} account', you can use the account information it contains to log into the system by clicking the 'Log in' button below. +LOGIN_RESET_ERRTITLE = Account reset failed +LOGIN_RESET_ERRSUMMARY = Password reset failed +LOGIN_RESET_ERRDESC = The system has been unable to reset your account. The error encountered was:

***reason*** + +# Activation resend +LOGIN_RESENDFORM = Resend activation code +LOGIN_RESENDINTRO = If you have accidentally deleted your activation email, or you have not received an an activation email more than 30 minutes after creating an account, enter your account email address below to be sent your activation code again.

IMPORTANT: requesting a new copy of your activation code will also reset your password. If you later receive the original registration email, the code and password it contains will not work and should be ignored. +LOGIN_RESENDEMAIL = Email address +LOGIN_DORESEND = Resend code +LOGIN_ERR_BADUSER = The email address provided does not appear to belong to any account in the system. +LOGIN_ERR_BADAUTH = The user account with the provided email address does not have a valid authentication method associated with it. This should not happen! +LOGIN_ERR_ALREADYACT = The user account with the provided email address is already active, and does not need a code to be activated. +LOGIN_RESEND_SUBJECT = Your {V_[sitename]} activation code +LOGIN_RESEND_GREET = Hi ***username*** +LOGIN_RESEND_INTRO = You, or someone pretending to be you, has requested that another copy of your activation code be sent to your email address. +LOGIN_RESEND_ALTACT = Alternatively, enter the following code in the account activation form: +LOGIN_RESEND_ENJOY = Thank you for registering! +LOGIN_RESEND_FAILED = Activation code resend failed + +LOGIN_RESEND_DONETITLE = Activation code resent +LOGIN_RESEND_SUMMARY = Resend successful! +LOGIN_RESEND_LONGDESC = A new password and an activation link have been send to your email address.

Please check your email for a message with the subject 'Your {V_[sitename]} activation code' and follow the instructions it contains to activate your account. +# Force password change +LOGIN_PASSCHANGE = Change password +LOGIN_FORCECHANGE_INTRO = Before you continue, please choose a new password to set for your account. +LOGIN_FORCECHANGE_TEMP = Your account is currently set up with a temporary password. +LOGIN_FORCECHANGE_OLD = The password on your account has expired as a result of age limits enforced by the site's password policy. +LOGIN_NEWPASSWORD = New password +LOGIN_CONFPASS = Confirm password +LOGIN_OLDPASS = Your current password +LOGIN_SETPASS = Change password +LOGIN_PASSCHANGE_FAILED = Password change failed + +LOGIN_PASSCHANGE_ERRNOUSER = No logged in user detected, password change unsupported. +LOGIN_PASSCHANGE_ERRMATCH = The new password specified does not match the confirm password. +LOGIN_PASSCHANGE_ERRSAME = The new password can not be the same as the old password. +LOGIN_PASSCHANGE_ERRVALID = The specified old password is not correct. You must enter the password you used to log in. + + +LOGIN_POLICY = Password policy +LOGIN_POLICY_INTRO = When choosing a new password, keep in mind that: +LOGIN_POLICY_NONE = No password policy is currently in place, you may use any password you want. +LOGIN_POLICY_MIN_LENGTH = Minimum length is ***value*** characters. +LOGIN_POLICY_MIN_LOWERCASE = At least ***value*** lowercase letters are needed. +LOGIN_POLICY_MIN_UPPERCASE = At least ***value*** uppercase letters are needed. +LOGIN_POLICY_MIN_DIGITS = At least ***value*** numbers must be included. +LOGIN_POLICY_MIN_OTHER = ***value*** non-alphanumeric chars are needed. +LOGIN_POLICY_MIN_ENTROPY = Passwords must pass a strength check. +LOGIN_POLICY_USE_CRACKLIB = Cracklib is used to test passwords. + +LOGIN_POLICY_MIN_LENGTHERR = Password is only ***set*** characters, minimum is ***require***. +LOGIN_POLICY_MIN_LOWERCASEERR = Only ***set*** of ***require*** lowercase letters provided. +LOGIN_POLICY_MIN_UPPERCASEERR = Only ***set*** of ***require*** uppercase letters provided. +LOGIN_POLICY_MIN_DIGITSERRR = Only ***set*** of ***require*** digits included. +LOGIN_POLICY_MIN_OTHERERR = Only ***set*** of ***require*** non-alphanumeric chars included. +LOGIN_POLICY_MIN_ENTROPYERR = The supplied password is not strong enough. +LOGIN_POLICY_USE_CRACKLIBERR = ***set*** + +LOGIN_POLICY_MAX_PASSWORDAGE = Passwords must be changed after ***value*** days. +LOGIN_POLICY_MAX_LOGINFAIL = You can log in incorrectly ***value*** times before your account needs reactivation. + +LOGIN_CRACKLIB_WAYSHORT = The password is far too short. +LOGIN_CRACKLIB_TOOSHORT = The password is too short. +LOGIN_CRACKLIB_MORECHARS = A greater range of characters are needed in the password. +LOGIN_CRACKLIB_WHITESPACE = Passwords can not be entirely whitespace! +LOGIN_CRACKLIB_SIMPLISTIC = The password is too simplistic or systematic. +LOGIN_CRACKLIB_NINUMBER = You can not use a NI number as a password. +LOGIN_CRACKLIB_DICTWORD = The password is based on a dictionary word. +LOGIN_CRACKLIB_DICTBACK = The password is based on a reversed dictionary word. + +# Login limiting +LOGIN_FAILLIMIT = You have used ***failcount*** of ***faillimit*** login attempts. If you exceed the limit, your account will be deactivated. If you can not remember your account details, please use the account recovery form +LOGIN_LOCKEDOUT = You have exceeded the number of login failures permitted by the system, and your account has been deactivated. An email has been sent to the address associated with your account explaining how to reactivate your account. +LOGIN_LOCKOUT_SUBJECT = {V_[sitename]} account locked +LOGIN_LOCKOUT_GREETING = Hi +LOGIN_LOCKOUT_MESSAGE = Your '{V_[sitename]}' account has deactivated and your password has been changed because more than ***faillimit*** login failures have been recorded for your account. This may be the result of attempted unauthorised access to your account - if you are not responsible for these login attempts you should probably contact the site administator to report that your account may be under attack. Your username and new password for the site are: +LOGIN_LOCKOUT_ACTNEEDED = As your account has been deactivated, before you can successfully log in you will need to reactivate your account. To do this, please click on the following link, or copy and paste it into your web browser: +LOGIN_LOCKOUT_ALTACT = Alternatively, enter the following code in the account activation form: + +# diff --git a/lang/en/navigation.lang b/lang/en/navigation.lang new file mode 100755 index 0000000..75d7a90 --- /dev/null +++ b/lang/en/navigation.lang @@ -0,0 +1,6 @@ +NAVBOX_PAGEOF = Page ***pagenum*** of ***maxpage*** +NAVBOX_FIRST = First +NAVBOX_PREV = Newer +NAVBOX_NEXT = Older +NAVBOX_LAST = Last +NAVBOX_SPACER = diff --git a/lang/en/permission.lang b/lang/en/permission.lang new file mode 100755 index 0000000..88d3edf --- /dev/null +++ b/lang/en/permission.lang @@ -0,0 +1,4 @@ +PERMISSION_FAILED_TITLE = Access denied +PERMISSION_FAILED_SUMMARY = You do not have permission to perform this operation. + +PERMISSION_VIEW_DESC = You do not have permission to view the requested resource. If you think this is incorrect, please contact {V_[admin_email]} for assistance. diff --git a/lang/en/userbar.lang b/lang/en/userbar.lang new file mode 100644 index 0000000..55f4d1b --- /dev/null +++ b/lang/en/userbar.lang @@ -0,0 +1,14 @@ +USERBAR_PROFILE_EDIT = Edit Profile +USERBAR_PROFILE_PREFS = Change Settings +USERBAR_PROFILE_LOGOUT = Log out +USERBAR_PROFILE_LOGIN = Log in + +USERBAR_LOGIN_USER = Username +USERBAR_LOGIN_PASS = Password +USERBAR_LOGIN = Log in + +USERBAR_DOCLINK = Documentation (opens in new window) + +USERBAR_FRONT = Aviary front page + +USERBAR_IMPORT = Import schedule diff --git a/lang/en/validate.lang b/lang/en/validate.lang new file mode 100755 index 0000000..ccc5797 --- /dev/null +++ b/lang/en/validate.lang @@ -0,0 +1,18 @@ +BLOCK_VALIDATE_NOTSET = No value provided for '***field***', this field is required. +BLOCK_VALIDATE_TOOSHORT = The value provided for '***field***' is too short. ***minlen*** or more characters must be provided for this field. +BLOCK_VALIDATE_TOOLONG = The value provided for '***field***' is too long. No more than ***maxlen*** characters can be provided for this field. +BLOCK_VALIDATE_BADCHARS = The value provided for '***field***' contains illegal characters. ***desc*** +BLOCK_VALIDATE_BADFORMAT = The value provided for '***field***' is not valid. ***desc*** +BLOCK_VALIDATE_DBERR = Unable to look up the value for '***field***' in the database. Error was: ***dberr***. +BLOCK_VALIDATE_BADOPT = The value selected for '***field***' is not a valid option. +BLOCK_VALIDATE_SCRUBFAIL = No content was left after cleaning the contents of html field '***field***'. +BLOCK_VALIDATE_TIDYFAIL = htmltidy failed for field '***field***'. +BLOCK_VALIDATE_CHKERRS = ***error*** html errors where encountered while validating '***field***'. Clean up the html and try again. +BLOCK_VALIDATE_CHKFAIL = Validation of '***field***' failed. Error from the W3C validator was: ***error***. +BLOCK_VALIDATE_NOTNUMBER = The value provided for '***field***' is not a valid number. +BLOCK_VALIDATE_RANGEMIN = The value provided for '***field***' is out of range (minimum is ***min***) +BLOCK_VALIDATE_RANGEMAX = The value provided for '***field***' is out of range (maximum is ***max***) + +BLOCK_ERROR_TITLE = Fatal System Error +BLOCK_ERROR_SUMMARY = The system has encountered an unrecoverable error. +BLOCK_ERROR_TEXT = A serious error has been encountered while processing your request. The following information was generated by the system, please contact moodlesupport@cs.man.ac.uk about this, including this error and a description of what you were doing when it happened!

***error*** diff --git a/makedocs.sh b/makedocs.sh new file mode 100755 index 0000000..5316252 --- /dev/null +++ b/makedocs.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Get the latest tag annotation out of git. +VERS=`git tag -n1 | sort -V | tail -n1 | perl -e '$tag = ; $tag =~ s/^.*?\s\s+(.*)$/$1/; print $tag;'` + +# Generate the documentation with the project number updated with the tag. +(cat supportfiles/Doxyfile; echo "PROJECT_NUMBER = \"$VERS\"") | doxygen - diff --git a/modules/.htaccess b/modules/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/modules/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/modules/ORB.pm b/modules/ORB.pm new file mode 100755 index 0000000..1720e6d --- /dev/null +++ b/modules/ORB.pm @@ -0,0 +1,773 @@ +## @file +# This file contains the implementation of the ORB block base class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# +package ORB; + +use strict; +use experimental qw(smartmatch); +use v5.14; + +use parent qw(Webperl::Block); # Features are just a specific form of Block +use CGI::Util qw(escape); +use HTML::Entities; +use Webperl::Utils qw(join_complex path_join); +use XML::Simple; +use DateTime; +use JSON; + +# Hack the DateTime object to include the TO_JSON function needed to support +# JSON output of datetime objects. Outputs as ISO8601 +sub DateTime::TO_JSON { + my $dt = shift; + + return $dt -> format_cldr('yyyy-MM-ddTHH:mm:ssZZZZZ'); +} + +# ============================================================================ +# Constructor + +## @cmethod $ new(%args) +# Overloaded constructor for ORB block modules. This will ensure that a valid +# item id has been stored in the block object data. +# +# @param args A hash of values to initialise the object with. See the Block docs +# for more information. +# @return A reference to a new ORB object on success, undef on error. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = $class -> SUPER::new(entitymap => { '–' => '-', + '—' => '-', + '’' => "'", + '‘' => "'", + '“' => '"', + '”' => '"', + '…' => '...', + '>' => '>', + '<' => '<', + '&' => '&', + ' ' => ' ', + }, + api_auth_header => 'Private-Token', + api_auth_keylen => 24, + @_) + or return undef; + + return $self; +} + + +# ============================================================================ +# HTML generation support + +## @method $ generate_cadence_page($title, $content, $extrahead, $doclink) +# A convenience function to wrap page content in the standard page template. This +# function allows blocks to embed their content in a page without having to build +# the whole page including "common" items themselves. It should be called to wrap +# the content when the block's page_display is returning. +# +# @param title The page title. +# @param content The content to show in the page. +# @param extrahead Any extra directives to place in the header. +# @param doclink The name of a document link to include in the userbar. If not +# supplied, no link is shown. +# @return A string containing the page. +sub generate_cadence_page { + my $self = shift; + my $title = shift; + my $content = shift; + my $extrahead = shift; + my $doclink = shift; + + my $userbar = $self -> {"module"} -> load_module("ORB::Userbar"); + + return $self -> {"template"} -> load_template("page.tem", {"%(extrahead)s" => $extrahead || "", + "%(title)s" => $title || "", + "%(userbar)s" => ($userbar ? $userbar -> block_display($title, $self -> {"block"}, $doclink) : ""), + "%(content)s" => $content}); +} + + +## @method $ generate_errorbox($message, $title) +# Generate the HTML to show in the page when a fatal error has been encountered. +# +# @param message The message to show in the page. +# @param title The title to use for the error. If not set "{L_FATAL_ERROR}" is used. +# @return A string containing the page +sub generate_errorbox { + my $self = shift; + my $message = shift; + my $title = shift || "{L_FATAL_ERROR}"; + + $self -> log("error:fatal", $message); + + $message = $self -> {"template"} -> message_box($title, + "error", + "{L_FATAL_ERROR_SUMMARY}", + $message, + undef, + "errorcore", + [ {"message" => $self -> {"template"} -> replace_langvar("SITE_CONTINUE"), + "colour" => "blue", + "action" => "location.href='{V_[scriptpath]}'"} ]); + my $userbar = $self -> {"module"} -> load_module("ORB::Userbar"); + + # Build the error page... + return $self -> {"template"} -> load_template("error/general.tem", + {"%(title)s" => $title, + "%(message)s" => $message, + "%(extrahead)s" => "", + "%(userbar)s" => ($userbar ? $userbar -> block_display($title) : ""), + }); +} + + +## @method $ generate_multiselect($name, $class, $idbase, $options, $selected) +# Generate a MultiSelect dropdown list (essentially a list of checkboxes that gets +# converted to a dropdown using the MultiSelect javascript module). +# +# @param name The name of the multiselect option list. +# @param class A class to add to the class attribute for the checkboxes in the list. +# @param idbase A unique base name for the ID of checkboxes in the list. +# @param options A reference to an array of option hashes. Each hash should contain +# `name` a short name used in the class, `id` a numeric ID used in the +# id and value attributes, and `desc` used in the label. +# @param selected A reference to a list of selected option IDs. +# @return A string containing the multiselect list checkboxes. +sub generate_multiselect { + my $self = shift; + my $name = shift; + my $class = shift; + my $idbase = shift; + my $options = shift; + my $selected = shift; + + # Convert the selected list to a hash for faster lookup + my %active = map { $_ => 1} @{$selected}; + + my $result = ""; + foreach my $option (@{$options}) { + $result .= $self -> {"template"} -> load_template("multisel-item.tem", {"%(class)s" => $class, + "%(idbase)s" => $idbase, + "%(selname)s" => $name, + "%(name)s" => $option -> {"name"}, + "%(id)s" => $option -> {"id"}, + "%(desc)s" => $option -> {"desc"}, + "%(checked)s" => $active{$option -> {"id"}} ? 'checked="checked"' : ''}); + } + + return $result; +} + + +# ============================================================================ +# Permissions/Roles related. + +## @method $ check_permission($action, $contextid, $userid) +# Determine whether the user has permission to peform the requested action. This +# should be overridden in subclasses to provide actual checks. +# +# @param action The action the user is attempting to perform. +# @param contextid The ID of the metadata context the user is trying to perform +# an action in. If this is not given, the root context is used. +# @param userid The ID of the user to check the permissions for. If not +# specified, the current session user is used. +# @return true if the user has permission, false if they do not, undef on error. +sub check_permission { + my $self = shift; + my $action = shift; + my $contextid = shift || $self -> {"system"} -> {"roles"} -> {"root_context"}; + my $userid = shift || $self -> {"session"} -> get_session_userid(); + + return $self -> {"system"} -> {"roles"} -> user_has_capability($contextid, $userid, $action); +} + + +## @method $ check_login() +# Determine whether the current user is logged in, and if not force them to +# the login form. +# +# @return undef if the user is logged in and has access, otherwise a page to +# send back with a permission error. If the user is not logged in, this +# will 'silently' redirect the user to the login form. +sub check_login { + my $self = shift; + + # Anonymous users need to get punted over to the login form + if($self -> {"session"} -> anonymous_session()) { + $self -> log("error:anonymous", "Redirecting anonymous user to login form"); + + print $self -> {"cgi"} -> redirect($self -> build_login_url()); + exit; + + # Otherwise, permissions need to be checked + } elsif(!$self -> check_permission("view")) { + $self -> log("error:permission", "User does not have perission 'view'"); + + # Logged in, but permission failed + my $message = $self -> {"template"} -> message_box("{L_PERMISSION_FAILED_TITLE}", + "error", + "{L_PERMISSION_FAILED_SUMMARY}", + "{L_PERMISSION_VIEW_DESC}", + undef, + "errorcore", + [ {"message" => $self -> {"template"} -> replace_langvar("SITE_CONTINUE"), + "colour" => "blue", + "action" => "location.href='{V_[scriptpath]}'"} ]); + my $userbar = $self -> {"module"} -> load_module("ORB::Userbar"); + + # Build the error page... + return $self -> {"template"} -> load_template("error/general.tem", + {"%(title)s" => "{L_PERMISSION_FAILED_TITLE}", + "%(message)s" => $message, + "%(extrahead)s" => "", + "%(userbar)s" => ($userbar ? $userbar -> block_display("{L_PERMISSION_FAILED_TITLE}") : ""), + }); + } + + return undef; +} + + +# ============================================================================ +# API support + +## @method $ is_api_operation() +# Determine whether the feature is being called in API mode, and if so what operation +# is being requested. +# +# @return A string containing the API operation name if the script is being invoked +# in API mode, undef otherwise. Note that, if the script is invoked in API mode, +# but no operation has been specified, this returns an empty string. +sub is_api_operation { + my $self = shift; + + my @api = $self -> {"cgi"} -> multi_param('api'); + + # No api means no API mode. + return undef unless(scalar(@api)); + + # API mode is set by placing 'api' in the first api entry. The second api + # entry is the operation. + return $api[1] || "" if($api[0] eq 'api'); + + return undef; +} + + +## @method $ api_param($param, $hasval, $params) +# Determine whether an API parameter has been set, and optionally return +# its value. This checks through the list of API parameters specified and, +# if the named parameter is present, this will either return the value +# that follows it in the parameter list if $hasval is true, or it will +# simply return true to indicate the parameter is present. +# +# @param param The name of the API parameter to search for. +# @param hasval If true, expect the value following the parameter in the +# list of parameters to be the value thereof, and return it. +# If false, this will return true if the parameter is present. +# @param params An optional reference to a list of parameters. If making +# multiple calls to api_param, grabbing the api parameter +# list beforehand and passing a reference to that into each +# api_param call will help speed the process up a bit. +# @return The value for the parameter if it is set and hasval is true, +# otherwise true if the paramter is present. If the parameter is +# not present, this will return undef. +sub api_param { + my $self = shift; + my $param = shift; + my $hasval = shift; + my $params = shift; + + if(!$params) { + my @api = $self -> {"cgi"} -> multi_param('api'); + return undef unless(scalar(@api)); + + $params = \@api; + } + + for(my $pos = 2; $pos < scalar(@{$params}); ++$pos) { + if($params -> [$pos] eq $param) { + return $hasval ? $params -> [$pos + 1] : 1; + } + } + + return undef; +} + + +## @method $ api_errorhash($code, $message) +# Generate a hash that can be passed to api_response() to indicate that an error was encountered. +# +# @param code A 'code' to identify the error. Does not need to be numeric, but it +# should be short, and as unique as possible to the error. +# @param message The human-readable error message. +# @return A reference to a hash to pass to api_response() +sub api_errorhash { + my $self = shift; + my $code = shift; + my $message = shift; + + return { 'error' => { + 'info' => $message, + 'code' => $code + } + }; +} + + +## @method $ api_html_response($data) +# Generate a HTML response containing the specified data. +# +# @param data The data to send back to the client. If this is a hash, it is +# assumed to be the result of a call to api_errorhash() and it is +# converted to an appropriate error box. Otherwise, the data is +# wrapped in a minimal html wrapper for return to the client. +# @return The html response to send back to the client. +sub api_html_response { + my $self = shift; + my $data = shift; + + # Fix up error hash returns + $data = $self -> {"template"} -> load_template("api/html_error.tem", {"%(code)s" => $data -> {"error"} -> {"code"}, + "%(info)s" => $data -> {"error"} -> {"info"}}) + if(ref($data) eq "HASH" && $data -> {"error"}); + + return $self -> {"template"} -> load_template("api/html_wrapper.tem", {"%(data)s" => $data}); +} + + +## @method private void _xml_api_response($data, %xmlopts) +# Print out the specified data as a XML response. +# +# @param data The data to send back to the client as XML. +# @param xmlopts Additional options passed to XML::Simple::XMLout. See the +# documentation for api_response() regarding this argument. +sub _xml_api_response { + my $self = shift; + my $data = shift; + my %xmlopts = shift; + my $xmldata; + + $xmlopts{"XMLDecl"} = '' + unless(defined($xmlopts{"XMLDecl"})); + + $xmlopts{"KeepRoot"} = 0 + unless(defined($xmlopts{"KeepRoot"})); + + $xmlopts{"RootName"} = 'api' + unless(defined($xmlopts{"RootName"})); + + eval { $xmldata = XMLout($data, %xmlopts); }; + $xmldata = $self -> {"template"} -> load_template("xml/error_response.tem", { "%(code)s" => "encoding_failed", + "%(error)s" => "Error encoding XML response: $@"}) + if($@); + + print $self -> {"cgi"} -> header(-type => 'application/xml', + -charset => 'utf-8'); + print Encode::encode_utf8($xmldata); +} + + +## @method private void _json_api_response($data) +# Print out the specified data as a JSON response. +# +# @param data The data to send back to the client as JSON. +sub _json_api_response { + my $self = shift; + my $data = shift; + + my $json = JSON -> new(); + print $self -> {"cgi"} -> header(-type => 'application/json', + -charset => 'utf-8'); + print Encode::encode_utf8($json -> pretty -> convert_blessed(1) -> encode($data)); +} + + +## @method $ api_response($data, %xmlopts) +# Generate an API response containing the specified data. This function will not return +# if it is successful - it will return an response and exit. The content generated by +# this function will be either JSON or XML depending on whether the user has specified +# an appropriate 'format=' argument, whether a system default default is set, falling back +# on JSON otherwise. +# +# @param data A reference to a hash containing the data to send back to the client as an +# API response. +# @param xmlopts Options passed to XML::Simple::XMLout if the respons is in XML. Note that +# the following defaults are set for you: +# - XMLDecl is set to '' +# - KeepRoot is set to 0 +# - RootName is set to 'api' +# @return Does not return if successful, otherwise returns undef. +sub api_response { + my $self = shift; + my $data = shift; + my @xmlopts = @_; + + # What manner of result should be resulting? + my $format = $self -> {"settings"} -> {"API:format"} || "json"; + $format = "json" if($self -> {"cgi"} -> param("format") && $self -> {"cgi"} -> param("format") =~ /^json$/i); + $format = "xml" if($self -> {"cgi"} -> param("format") && $self -> {"cgi"} -> param("format") =~ /^xml$/i); + + given($format) { + when("xml") { $self -> _xml_api_response($data, @xmlopts); } + default { $self -> _json_api_response($data); } + } + + $self -> {"template"} -> set_module_obj(undef); + $self -> {"messages"} -> set_module_obj(undef); + $self -> {"system"} -> clear() if($self -> {"system"}); + $self -> {"session"} -> {"auth"} -> {"app"} -> set_system(undef) if($self -> {"session"} -> {"auth"} -> {"app"}); + + $self -> {"dbh"} -> disconnect(); + $self -> {"logger"} -> end_log(); + + exit; +} + + +## @method $ api_token_login() +# Determine whether the client has sent an API token as part of the http request, and +# if so establish whether the key is valid and corresponds to a user in the system. +# This will set up the global session object to be 'logged in' as the key owner, +# if they key is valid. Note that methods that rely on or generate session cookies +# are not going to operate correctly when this is used: use only for API code! +# +# @note If using token auth, https *must* be used, or you may as well remove the +# auth code entirely. +# +# @return The ID of the user the token corresponds to on success, undef if the user +# has not provided a token header, or the token is not valid. +sub api_token_login { + my $self = shift; + + $self -> clear_error(); + + my $key = $self -> {"cgi"} -> http($self -> {"api_auth_header"}); + return undef unless($key); + + my ($checkkey) = $key =~ /^(\w+)$/; + return undef unless($checkkey && length($checkkey) == $self -> {"api_auth_keylen"}); + + my $keyrec = $self -> {"dbh"} -> prepare("SELECT `user_id` + FROM `".$self -> {"settings"} -> {"database"} -> {"apikeys"}."` + WHERE `token` = ? + AND `active` = 1 + ORDER BY `created` DESC + LIMIT 1"); + $keyrec -> execute($checkkey) + or return $self -> self_error("Unable to look up api key: ".$self -> {"dbh"} -> errstr()); + + my $keydata = $keyrec -> fetchrow_hashref() + or return $self -> self_error("No matching api key record when looking for key '$checkkey'"); + + # This is a bit of a hack, but as long as it is called before any other session + # code in the API module, it'll fake a logged-in session. + $self -> {"session"} -> {"sessuser"} = $keydata -> {"user_id"}; + + return $keydata -> {"user_id"}; +} + + +## @method $ api_token_generate($userid) +# Generate a guaranteed-unique API token/key for the specified user. This will record the +# new token in the database for later use, deactivating any previously-issued tokens for +# the user, and return a copy of the new token. +# +# @param userid The ID of the user to generate a token for +# @return The new token string on success, undef on error. +sub api_token_generate { + my $self = shift; + my $userid = shift; + my $token = ''; + + $self -> clear_error(); + + my $checkh = $self -> {"dbh"} -> prepare("SELECT `user_id` + FROM `".$self -> {"settings"} -> {"database"} -> {"apikeys"}."` + WHERE `token` = ?"); + + # Generate tokens until we hit one that isn't already defined. + do { + $token = join("", map { ("a".."z", "A".."Z", 0..9)[rand 62] } 1..$self -> {"api_auth_keylen"}); + + $checkh -> execute($token) + or return $self -> self_error("Unable to look up api token: ".$self -> {"dbh"} -> errstr()); + + } while($checkh -> fetchrow_hashref()); + + # Deactivate the user's old tokens + my $blockh = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {"apikeys"}."` + SET `active` = 0 + WHERE `active` = 1 AND `user_id` = ?"); + $blockh -> execute($userid) + or return $self -> self_error("Unable to deactivate old api tokens: ".$self -> {"dbh"} -> errstr()); + + # And add the new token + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {"apikeys"}."` + (`user_id`, `token`, `created`) + VALUES(?, ?, UNIX_TIMESTAMP())"); + + my $row = $newh -> execute($userid, $token); + return $self -> self_error("Unable to store token '$token' for user '$userid': ".$self -> {"dbh"} -> errstr) if(!$row); + return $self -> self_error("Insert failed for token '$token' for user '$userid': no rows inserted") if($row eq "0E0"); + + return $token; +} + + +# ============================================================================ +# General utility + +## @method void log($type, $message) +# Log the current user's actions in the system. This is a convenience wrapper around the +# Logger::log function. +# +# @param type The type of log entry to make, may be up to 64 characters long. +# @param message The message to attach to the log entry, avoid messages over 128 characters. +sub log { + my $self = shift; + my $type = shift; + my $message = shift; + + $message = "[Item:".($self -> {"itemid"} ? $self -> {"itemid"} : "none")."] $message"; + $self -> {"logger"} -> log($type, $self -> {"session"} -> get_session_userid(), $self -> {"cgi"} -> remote_host(), $message); +} + + +## @method $ set_saved_state() +# Store the current status of the script, including block, api, pathinfo, and querystring +# to session variables for later restoration. +# +# @return true on success, undef on error. +sub set_saved_state { + my $self = shift; + + $self -> clear_error(); + + my $res = $self -> {"session"} -> set_variable("saved_block", $self -> {"cgi"} -> param("block")); + return undef unless(defined($res)); + + my @pathinfo = $self -> {"cgi"} -> param("pathinfo"); + $res = $self -> {"session"} -> set_variable("saved_pathinfo", join("/", @pathinfo)); + return undef unless(defined($res)); + + my @api = $self -> {"cgi"} -> param("api"); + $res = $self -> {"session"} -> set_variable("saved_api", join("/", @api)); + return undef unless(defined($res)); + + # Convert the query parameters to a string, skipping the block, pathinfo, and api + my @names = $self -> {"cgi"} -> param; + my @qstring = (); + foreach my $name (@names) { + next if($name eq "block" || $name eq "pathinfo" || $name eq "api"); + + my @vals = $self -> {"cgi"} -> param($name); + foreach my $val (@vals) { + push(@qstring, escape($name)."=".escape($val)); + } + } + $res = $self -> {"session"} -> set_variable("saved_qstring", join("&", @qstring)); + return undef unless(defined($res)); + + return 1; +} + + +## @method @ get_saved_state() +# A convenience wrapper around Session::get_variable() for fetching the state saved in +# build_login_url(). +# +# @return An array of strings, containing the block, pathinfo, api, and query string. +sub get_saved_state { + my $self = shift; + + # Yes, these use set_variable. set_variable will return the value in the + # variable, like get_variable, except that this will also delete the variable + return ($self -> {"session"} -> set_variable("saved_block"), + $self -> {"session"} -> set_variable("saved_pathinfo"), + $self -> {"session"} -> set_variable("saved_api"), + $self -> {"session"} -> set_variable("saved_qstring")); +} + + +## @method $ cleanup_entities($html) +# Wrangle the specified HTML into something that won't produce an unholy mess when +# passed to something that doesn't handle UTF-8 properly. +# +# @param html The HTML to process +# @return A somewhat cleaned-up string of HTML +sub cleanup_entities { + my $self = shift; + my $html = shift; + + $html =~ s/\r//g; + return encode_entities($html, '^\n\x20-\x7e'); +} + + +# ============================================================================ +# URL building + +## @method $ build_login_url() +# Attempt to generate a URL that can be used to redirect the user to a login form. +# The user's current query state (course, block, etc) is stored in a session variable +# that can later be used to bring them back to the location this was called from. +# +# @return A relative login form redirection URL. +sub build_login_url { + my $self = shift; + + # Store as much state as possible to restore after login (does not store POST + # data!) + $self -> set_saved_state(); + + return $self -> build_url(block => "login", + fullurl => 1, + pathinfo => [], + params => {}, + forcessl => 1); +} + + +## @method $ build_return_url($fullurl) +# Pulls the data out of the session saved state, checks it for safety, +# and returns the URL the user should be redirected/linked to to return to the +# location they were attempting to access before login. +# +# @param fullurl If set to true, the generated url will contain the protocol and +# host. Otherwise the URL will be absolute from the server root. +# @return A relative return URL. +sub build_return_url { + my $self = shift; + my $fullurl = shift; + my ($block, $pathinfo, $api, $qstring) = $self -> get_saved_state(); + + # Return url block should never be "login" + $block = $self -> {"settings"} -> {"config"} -> {"default_block"} if($block eq "login" || !$block); + + # Build the URL from them + return $self -> build_url("block" => $block, + "pathinfo" => $pathinfo, + "api" => $api, + "params" => $qstring, + "fullurl" => $fullurl); +} + + +## @method $ build_url(%args) +# Build a url suitable for use at any point in the system. This takes the args +# and attempts to build a url from them. Supported arguments are: +# +# * fullurl - if set, the resulting URL will include the protocol and host. Defaults to +# false (URL is absolute from the host root). +# * block - the name of the block to include in the url. If not set, the current block +# is used if possible, otherwise the system-wide default block is used. +# * pathinfo - Either a string containing the pathinfo, or a reference to an array +# containing pathinfo fragments. If not set, the current pathinfo is used. +# * api - api fragments. If the first element is not "api", it is added. +# * params - Either a string containing additional query string parameters to add to +# the URL, or a reference to a hash of additional query string arguments. +# Values in the hash may be references to arrays, in which case multiple +# copies of the parameter are added to the query string, one for each +# value in the array. +# * forcessl - If true, the URL is forced to https: rather than http: +# +# @param args A hash of arguments to use when building the URL. +# @return A string containing the URL. +sub build_url { + my $self = shift; + my %args = @_; + my $base = ""; + + # Default the block, item, and API fragments if needed and possible + $args{"block"} = ($self -> {"cgi"} -> param("block") || $self -> {"settings"} -> {"config"} -> {"default_block"}) + if(!defined($args{"block"})); + + if(!defined($args{"pathinfo"})) { + my @cgipath = $self -> {"cgi"} -> multi_param("pathinfo"); + $args{"pathinfo"} = \@cgipath if(scalar(@cgipath)); + } + + if(!defined($args{"api"})) { + my @cgiapi = $self -> {"cgi"} -> multi_param("api"); + $args{"api"} = \@cgiapi if(scalar(@cgiapi)); + } + + # Convert the pathinfo and api to slash-delimited strings + my $pathinfo = join_complex($args{"pathinfo"}, joinstr => "/"); + my $api = join_complex($args{"api"}, joinstr => "/"); + + # Force the API call to start 'api' if it doesn't + $api = "api/$api" if($api && $api !~ m|^/?api|); + + # build the query string parameters. + my $querystring = join_complex($args{"params"}, joinstr => ($args{"joinstr"} || "&"), pairstr => "=", escape => 1); + + # building the URL involves shoving the bits together. path_join is intelligent enough to ignore + # anything that is undef or "" here, so explicit checks beforehand should not be needed. + my $url = path_join($self -> {"settings"} -> {"config"} -> {"scriptpath"}, $args{"block"}, $pathinfo, $api); + $url = path_join($self -> {"cgi"} -> url(-base => 1), $url) + if($args{"fullurl"}); + + # Strip block, pathinfo, and api from the query string if they've somehow made it in there. + # Note this can't simply be made 'eg' as the progressive match can leave a trailing & + if($querystring) { + while($querystring =~ s{((?:&(?:amp;))?)(?:api|block|pathinfo)=[^&]+(&?)}{$1 && $2 ? "&" : ""}e) {} + $url .= "?$querystring"; + } + + $url =~ s/^http:/https:/ + if($args{"forcessl"} && $url =~ /^http:/); + + return $url; +} + + +# ============================================================================ +# Documentation support + +## @method $ get_documentation_url($doclink) +# Given a documentation link name, obtain the URL associated with that name. +# +# @param doclink The name of the documentation link to fetch. +# @return The documentation URL if the doclink is valid, undef otherwise. +sub get_documentation_url { + my $self = shift; + my $doclink = shift; + + $self -> clear_error(); + + # No point trying anything if there is no link name set. + return undef if(!$doclink); + + my $urlh = $self -> {"dbh"} -> prepare("SELECT `url` + FROM `".$self -> {"settings"} -> {"database"} -> {"docs"}."` + WHERE `name` LIKE ?"); + $urlh -> execute($doclink) + or return $self -> self_error("Unable to look up documentation link: ".$self -> {"dbh"} -> errstr); + + # Fetch the url row, and if one has been found return it. + my $url = $urlh -> fetchrow_arrayref(); + return $url ? $url -> [0] : undef; +} + + +1; diff --git a/modules/ORB/AppUser.pm b/modules/ORB/AppUser.pm new file mode 100755 index 0000000..98eae24 --- /dev/null +++ b/modules/ORB/AppUser.pm @@ -0,0 +1,197 @@ +## @file +# This file contains the ORB-specific user handling. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +package ORB::AppUser; + +use strict; +use parent qw(Webperl::AppUser); +use Digest::MD5 qw(md5_hex); +use Webperl::Utils qw(trimspace); + + +## @method $ get_user($username, $onlyreal) +# Obtain the user record for the specified user, if they exist. This returns a +# reference to a hash of user data corresponding to the specified userid, +# or undef if the userid does not correspond to a valid user. If the onlyreal +# argument is set, the userid must correspond to 'real' user - bots or inactive +# users are not be returned. +# +# @param username The username of the user to obtain the data for. +# @param onlyreal If true, only users of type 0 or 3 are returned. +# @return A reference to a hash containing the user's data, or undef if the user +# can not be located (or is not real) +sub get_user { + my $self = shift; + my $username = shift; + my $onlyreal = shift; + + my $user = $self -> _get_user("username", $username, $onlyreal, 1) + or return undef; + + return $self -> _make_user_extradata($user); +} + + +## @method $ get_user_byid($userid, $onlyreal) +# Obtain the user record for the specified user, if they exist. This returns a +# reference to a hash of user data corresponding to the specified userid, +# or undef if the userid does not correspond to a valid user. If the onlyreal +# argument is set, the userid must correspond to 'real' user - bots or inactive +# users are not be returned. +# +# @param userid The id of the user to obtain the data for. +# @param onlyreal If true, only users of type 0 or 3 are returned. +# @return A reference to a hash containing the user's data, or undef if the user +# can not be located (or is not real) +sub get_user_byid { + my $self = shift; + my $userid = shift; + my $onlyreal = shift; + + # obtain the user record + my $user = $self -> _get_user("user_id", $userid, $onlyreal) + or return undef; + + return $self -> _make_user_extradata($user); +} + + +## @method $ get_user_byemail($email, $onlyreal) +# Obtain the user record for the user with the specified email, if available. +# This returns a reference to a hash containing the user data corresponding +# to the user with the specified email, or undef if no users have the email +# specified. If the onlyreal argument is set, the userid must correspond to +# 'real' user - bots or inactive users should not be returned. +# +# @param email The email address to find an owner for. +# @param onlyreal If true, only users of type 0 or 3 are returned. +# @return A reference to a hash containing the user's data, or undef if the email +# address can not be located (or is not real) +sub get_user_byemail { + my $self = shift; + my $email = shift; + my $onlyreal = shift; + + my $user = $self -> _get_user("email", $email, $onlyreal, 1) + or return undef; + + return $self -> _make_user_extradata($user); +} + + +## @method $ post_authenticate($username, $password, $auth, $extradata) +# After the user has logged in, ensure that they have an in-system record. +# This is essentially a wrapper around the standard AppUser::post_authenticate() +# that handles things like user account activation checks. +# +# @param username The username of the user to perform post-auth tasks on. +# @param password The password the user authenticated with. +# @param auth A reference to the auth object calling this. +# @param authmethod The id of the authmethod to set for the user. +# @param extradata A reference to a hash of extra data fields for the user. +# @return A reference to a hash containing the user's data on success, +# undef otherwise. If this returns undef, an error message will be +# set in to the specified auth's errstr field. +sub post_authenticate { + my $self = shift; + my $username = shift; + my $password = shift; + my $auth = shift; + my $authmethod = shift; + my $extradata = shift; + + # Let the superclass handle user creation + my $user = $self -> SUPER::post_authenticate($username, $password, $auth, $authmethod, $extradata); + return undef unless($user); + + # User now exists, determine whether the user is active + return $self -> post_login_checks($user, $auth) + if($user -> {"activated"}); + + # User is inactive, does the account need activating? + if(!$user -> {"act_code"}) { + # No code provided, so just activate the account + if($self -> activate_user_byid($user -> {"user_id"})) { + return $user; #$self -> post_login_checks($user, $auth) + } else { + return $auth -> self_error($self -> {"errstr"}); + } + } else { + return $auth -> self_error("User account is not active."); + } +} + + +## @method $ post_login_checks($user, $auth) +# Perform checks on the specified user after they have logged in (post_authenticate is +# going to return the user record). This ensures that the user has the appropriate +# roles and settings. +# +# @param user A reference to a hash containing the user's data. +# @param auth A reference to the auth object calling this. +# @return A reference to a hash containing the user's data on success, +# undef otherwise. If this returns undef, an error message will be +# set in to the specified auth's errstr field. +sub post_login_checks { + my $self = shift; + my $user = shift; + my $auth = shift; + + # All users must have the user role in the metadata root + my $roleid = $self -> {"system"} -> {"roles"} -> role_get_roleid("user"); + my $root = $self -> {"system"} -> {"roles"} -> {"root_context"}; + my $hasrole = $self -> {"system"} -> {"roles"} -> user_has_role($root, $user -> {"user_id"}, $roleid); + + # Give up if the role check failed. + return $auth -> self_error($self -> {"system"} -> {"roles"} -> {"errstr"}) + if(!defined($hasrole)); + + # Try to assign the role if the user does not have it. + $self -> {"system"} -> {"roles"} -> user_assign_role($root, $user -> {"user_id"}, $roleid) + or return $auth -> self_error($self -> {"system"} -> {"roles"} -> {"errstr"}) + if(!$hasrole); + + # TODO: Assign other roles as needed. + + return $user; +} + + +# ============================================================================ +# Internal functions + +## @method private $ _make_user_extradata($user) +# Generate the 'calculated' user fields - full name, gravatar hash, etc. +# +# @param user A reference to the user hash to work on. +# @return The user hash reference. +sub _make_user_extradata { + my $self = shift; + my $user = shift; + + # Generate the user's full name + $user -> {"fullname"} = $user -> {"realname"} || $user -> {"username"}; + + # Make the user gravatar hash + $user -> {"gravatar_hash"} = md5_hex(lc(trimspace($user -> {"email"} || ""))); + + return $user; +} + +1; diff --git a/modules/ORB/BlockSelector.pm b/modules/ORB/BlockSelector.pm new file mode 100755 index 0000000..24fc0ab --- /dev/null +++ b/modules/ORB/BlockSelector.pm @@ -0,0 +1,115 @@ +## @file +# This file contains the ORB-specific implementation of the runtime +# block selection class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# Select the appropriate block to render a page based on an ORB URL. +# This allows a url of the form /block/item/path/?args to be parsed into +# something the ORB classes can use to render pages properly, and +# select the appropriate block for the current request. +package ORB::BlockSelector; + +use strict; +use parent qw(Webperl::BlockSelector); + +# ============================================================================ +# Block Selection + +## @method $ get_block($dbh, $cgi, $settings, $logger, $session) +# Determine which block to use to generate the requested page. This performs +# the same task as BlockSelector::get_block(), except that it will also parse +# the contents of the PATH_INFO environment variable into the query string +# data, allowing ORB paths to be passed to the rest of the code without +# the need to check both the query string and PATH_INFO. +# +# After this has been called, the following variables may be set in the cgi +# object: +# +# - `block` contains the currently selected block name, or the gallery block name +# if one has not been specified. +# - `pathinfo` contains the path to the currently selected item, as an array +# of path segments. If not set, no item has been selected. Note that +# this is simply a split version of any path info between the block and any +# api specification, so it may be used by blocks to mean something other than +# the item selected if needed. +# - `pathinfo` contains any API call data, if any, as an array of path items. +# If this is set, the first item will be the literal `api`, while the remaining +# items will be the API operation and arguments. +# +# @param dbh A reference to the database handle to issue queries through. +# @param cgi A reference to the system CGI object. +# @param settings A reference to the global settings object. +# @param logger A reference to the system logger object. +# @param session A reference to the session object. +# @return The id or name of the block to use to render the page, or undef if +# an error occurred while selecting the block. +sub get_block { + my $self = shift; + my $dbh = shift; + my $cgi = shift; + my $settings = shift; + my $logger = shift; + my $session = shift; + + $self -> self_error(""); + + my $pathinfo = $ENV{'PATH_INFO'}; + + # If path info is present, it needs to be shoved into the cgi object + if($pathinfo) { + # strip off the script if it is present + $pathinfo =~ s|^(/media)?/index.cgi||; + + # pull out the api if specified + my ($apicall) = $pathinfo =~ m|/(api.*)$|; + $pathinfo =~ s|/api.*|| if($apicall); + + # No need for leading /, it'll just confuse the split + $pathinfo =~ s|^/||; + + # Split along slashes + my @args = split(/\//, $pathinfo); + + # Defaults the block to the gallery, and clear the pathinfo and pathinfo for safety + my $block = $settings -> {"config"} -> {"gallery_block"}; + $cgi -> delete('pathinfo', 'api'); + + # If a single item remains in the argument, it is a block name + if(scalar(@args) == 1) { + $block = shift @args; + + # Two or more items in the argument are a block and item path + } elsif(scalar(@args) >= 2) { + $block = shift @args; + $cgi -> param(-name => 'pathinfo', -values => \@args); + } + + $cgi -> param(-name => 'block', -value => $block); + + # Now sort out the API + if($apicall) { + my @api = split(/\//, $apicall); + $cgi -> param(-name => 'api', -values => \@api); + } + } + + # The behaviour of BlockSelector::get_block() is fine, so let it work out the block + return $self -> SUPER::get_block($dbh, $cgi, $settings, $logger); +} + +1; diff --git a/modules/ORB/System.pm b/modules/ORB/System.pm new file mode 100755 index 0000000..7a70297 --- /dev/null +++ b/modules/ORB/System.pm @@ -0,0 +1,70 @@ +## @file +# This file contains the ORB-specific implementation of the runtime +# application-specific module loader class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# Loads any system-wide application specific modules needed by the +# ORB application. +package ORB::System; + +use strict; +use parent qw(Webperl::System); + +use ORB::System::Metadata; +use ORB::System::Roles; + + +## @method $ init(%args) +# Initialise the ORB System's references to other system objects. This +# sets up the ORB-specific modules, placing references to them into the +# object's hash. The argument hash provided must minimally contain the +# following references: +# +# * cgi, a reference to a CGI object. +# * dbh, a reference to the DBI object to issue database queries through. +# * settings, a reference to the global settings object. +# * logger, a reference to a Logger object. +# * template, a reference to the system template engine. +# * session, a reference to the system session handler. +# * modules, a reference to the module loader. +# +# @param args A hash of arguments to initialise the System object with. +# @return true on success, false if something failed. If this returns false, +# the reason is in $self -> {"errstr"}. +sub init { + my $self = shift; + + # Let the superclass copy the references over + $self -> SUPER::init(@_) + or return undef; + + $self -> {"metadata"} = ORB::System::Metadata -> new(dbh => $self -> {"dbh"}, + settings => $self -> {"settings"}, + logger => $self -> {"logger"}) + or return $self -> self_error("Metadata system init failed: ".$Webperl::SystemModule::errstr); + + $self -> {"roles"} = ORB::System::Roles -> new(dbh => $self -> {"dbh"}, + settings => $self -> {"settings"}, + logger => $self -> {"logger"}, + metadata => $self -> {"metadata"}) + or return $self -> self_error("Roles system init failed: ".$Webperl::SystemModule::errstr); + + return 1; +} + +1; diff --git a/modules/ORB/System/Entity.pm b/modules/ORB/System/Entity.pm new file mode 100644 index 0000000..dc910c5 --- /dev/null +++ b/modules/ORB/System/Entity.pm @@ -0,0 +1,376 @@ +## @file +# This file contains the implementation of the Entity base class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class Entity +# This is a base class for entities in the system, providing common +# features for all of the entities. Entities are any simple named +# object in the system, typically things like ingredients, prep +# memthods, recipe types and states. +# +# Tables for entities must have a minimum of the following fields: +# +# - `id`: unsigned int or larger, auto incremement +# - `name`: varchar ot text, utf8_unicode_ci charset recommended +# - `refcount`: unsigned int recommended +# +package ORB::System::Entity; + +use strict; +use parent qw(Webperl::SystemModule); +use v5.14; + +use Webperl::Utils qw(hash_or_hashref); + + +# ============================================================================ +# Constructor + +## @cmethod $ new(%args) +# Create a new Entity object to manage entity creation and management. +# The minimum values you need to provide are: +# +# - `dbh` - The database handle to use for queries. +# - `settings` - The system settings object +# - `logger` - The system logger object. +# - `entity_table` - The name of the table the entities are stored in. +# +# @param args A hash of key value pairs to initialise the object with. +# @return A new Entity object, or undef if a problem occured. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = $class -> SUPER::new(@_); + + return SystemModule::set_error("No entity table specified when attempting to create object") + if(!$self -> {"entity_table"}); + + return $self +} + + +# ============================================================================ +# Entity creation and deletion + +## @method $ create($name) +# Attempt to create a new named entity in the entoty table. Generally you +# should not call this directly, as it will create a new entity in the table +# even if an entity already exists with the same name: you will generally +# want to call get_id() instead, as that will determine whether that the +# entity already exists before calling this if it does not. +# +# @param name The name of the entity to add. +# @return The new entity ID on success, undef on error. +sub create { + my $self = shift; + my $name = shift; + + $self -> clear_error(); + + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {$self -> {"entity_table"}}."` + (`name`) + VALUES(?)"); + my $rows = $newh -> execute($name); + return $self -> self_error("Unable to perform ".$self -> {"entity_table"}." insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error($self -> {"entity_table"}." insert failed, no rows inserted") if($rows eq "0E0"); + + # FIXME: This ties to MySQL, but is more reliable that last_insert_id in general. + # Try to find a decent solution for this mess... + # NOTE: the DBD::mysql documentation doesn't actually provide any useful information + # about what this will contain if the insert fails. In fact, DBD::mysql calls + # libmysql's mysql_insert_id(), which returns 0 on error (last insert failed). + # There, why couldn't they bloody /say/ that?! + my $id = $self -> {"dbh"} -> {"mysql_insertid"}; + return $self -> self_error("Unable to obtain id for entity '$name'") if(!$id); + + return $id; +} + + +## @method $ destroy(%args) +# Attempt to remove the specified entity, and any assignments of it, from the system. +# Supported arguments are: +# +# - `id`: The ID of the entity to remove. If not specified, a name must be given. +# - `name`: The name of the entity to remove. If ID is specified, this is ignored. +# - `relations`: A hash containing `name` and `field` fields specifying the table +# to remove any relations from. This may be specified as a +# reference to an array of hashes. +# +# @param args A hash, or reference to a hash, or argument. +# @return true on success, undef on error +sub destroy { + my $self = shift; + my $args = hash_or_hashref(@_); + + $self -> clear_error(); + + return $self -> self_error("No id or name specified in call to destroy") + unless($args -> {"id"} || $args -> {"name"}); + + # Convert name to ID if needed + $args -> {"id"} = $self -> _fetch_id($args -> {"name"}) + unless($args -> {"id"}); + + # fall over if the relations argument is specified, but it's not a hash or arrayref + return $self -> self_error("destroy invoked with invalid relations data") + if($args -> {"relations"} && + ref($args -> {"relations"} ne "HASH" && ref($args -> {"relations"}) ne "ARRAY")); + + # Force arrayref of hashes for simplicity + $args -> {"relations"} = [ $args -> {"relations"} ] + if($args -> {"relations"} && ref($args -> {"relations"} eq "HASH")); + + # Process and remove each relation... or possibly none if there are none. + foreach my $relation (@{$args -> {"relations"}}) { + return $self -> self_error("Relation hash data invalid") + unless($relation -> {"table"} && $relation -> {"field"}); + + $self -> remove_relation($args -> {"id"}, $relation -> {"table"}, $relation -> {"field"}) + or return undef; + } + + # Check that the entity is safe to delete... + my $refcount = $self -> _fetch_refcount($id); + + return $self -> self_error("Attempt to delete non-existent entity $id from ".$self -> {"entity_table"}) + unless(defined($refcount)); + + return $self -> self_error("Attempt to delete entity $id in ".$self -> {"entity_table"}." while still in use ($refcount references)") + if($refcount); + + # And now delete the entity itself + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM `".$self -> {"settings"} -> {"database"} -> {$self -> {"entity_table"}}."` + WHERE `id` = ?"); + $nukeh -> execute($args -> {"id"}) + or return $self -> self_error("Unable to perform entity $id removal from ".$self -> {"entity_table"}.": ". $self -> {"dbh"} -> errstr); + + return 1; +} + + +# ============================================================================ +# Lookup + +## @method $ get_id($name) +# Obtain the ID associated with the specified entity. If the entity +# does not yet exist in the entity's table, this will create it and +# return the id the new entity was allocated. +# +# @param name The name of the entity to obtain the ID for +# @return The ID of the entity on success, undef on error. +sub get_id { + my $self = shift; + my $name = shift; + + my $id = $self -> _fetch_id($name); + return $id if(!defined($id) || $id); # return undefs, or non-zero ids. + + # Get here, and $id is zero - no entity exists with the specified name, so + # a new entity is needed. + return $self -> create($name); +} + + +# ============================================================================ +# Relation handling + +## @method $ remove_relation($id, $table, $field, $retain_unused) +# Remove any relations to the specified entity from the table provided. +# This will decrease the refcount for the entity. +# +# @param id The Id of the entity to remove the relation for. +# @param table The name of the table containing the relation to remove. +# @param field The field containing the entity ID in the relation table. +# @param retain_unused If true, do not delete the entity even if its refcount will +# be zero after calling this. Defaults to true. +# @return true on success, false on error. +sub remove_relation { + my $self = shift; + my $id = shift; + my $table = shift; + my $field = shift; + my $retain_unused = shift // 1; + + $self -> clear_error(); + + my $removeh = $self -> {"dbh"} -> prepare("DELETE FROM `$table` + WHERE `$field` = ?"); + my $rows = $removeh -> execute($id); + return $self -> self_error("Unable to remove entity relations to $id from $table") if(!$rows); + return 1 if($rows eq "0E0"); # Zero row removal is possible and not an error + + my $result = $self -> _update_refcount($id, subtract => $rows); + return undef if(!defined($result)); + + # Nuke the entity if there's no reason to keep it around. + return $self -> destroy($id) + unless($retain_unused || $result); + + return defined($result); +} + +# ============================================================================ +# Reference counting + +## @method $ increase_refcount($id) +# Increase the refcount for the entity with the specified id. +# +# @param id The ID of the entity to increase the refcount for. +# @return true on success (actually, the reference count), false on error. +sub increase_refcount { + my $self = shift; + my $id = shift; + + return $self -> _update_refcount($id, add => 1); +} + + +## @method $ decrease_refcount($id, $retain_unused) +# Reduce the refcount for the entity with the specified id. If the refcount +# becomes zero, and $retain_unused is not true, the entity is removed from +# the system. +# +# @param id The ID of the entity to decrease the refcount for. +# @param retain_unused If true, do not delete the entity even if its refcount will +# be zero after calling this. Defaults to true. +# @return true on success, false on error. +sub decrease_refcount { + my $self = shift; + my $id = shift; + my $retain_unused = shift // 1; + + # Change the refcount, bomb if the change failed. + my $result = $self -> _update_refcount($id, subtract => 1); + return undef if(!defined($result)); + + # Nuke the entity if there's no reason to keep it around. + return $self -> destroy($id) + unless($retain_unused || $result); + + return defined($result); +} + + +# ============================================================================ +# ID lookup + +## @method protected $ _fetch_id($name) +# Given an entity name, attempt to find a record for that name. This will locate the +# first defined entity whose name matches the provided name. Note that if there are +# duplicate entities in the system, this will never find duplicates - it is guaranteed to +# find the entity with the lowest ID whose name matches the provided value, or nothing. +# +# @param name The name of the entity to find. +# @return The ID of the entity with the specified name on success, 0 if it does not exist, +# undef on error occurred. +sub _fetch_id { + my $self = shift; + my $name = shift; + + $self -> clear_error(); + + # Simple lookup. If the name field is set up as a _ci field, this will be case insensitive + my $entityh = $self -> {"dbh"} -> prepare("SELECT `id` + FROM `".$self -> {"settings"} -> {"database"} -> {$self -> {"entity_table"}}."` + WHERE `name` LIKE ? + ORDER BY `id` + LIMIT 1"); # Limit to avoid pulling multiple rows if there are duplicates. + $entityh -> execute($name) + or return $self -> self_error("Unable to perform entity lookup: ".$self -> {"dbh"} -> errstr); + + # Return the ID if we have located it + my $entityrow = $entityh-> fetchrow_arrayref(); + return $entityrow ? $entityrow -> [0] : 0; +} + + +# ============================================================================ +# Reference handling - generally intended only for subclasses to use + +## @method protected $ _fetch_refcount($id) +# Obtain the reference count for the specified entity. This will attempt to +# fetch the reference count for the entity, if it fails - because the entity +# does not exist, or the database has shat itself - it will return undef. +# +# @param id The entity to fetch the reference count for. +# @return The number of references to the entity (which may be zero), or undef +# on error. +sub _fetch_refcount { + my $self = shift; + my $id = shift; + + $self -> clear_error(); + + # Simple lookup, nothing spectacular to see here... + my $refh = $self -> {"dbh"} -> prepare("SELECT `refcount` + FROM `".$self -> {"settings"} -> {"database"} -> {$self -> {"entity_table"}}."` + WHERE `id` = ?"); + $refh -> execute($id) + or return $self -> self_error("Unable to perform entity refcount lookup: ". $self -> {"dbh"} -> errstr); + + my $refcount = $refh -> fetchrow_arrayref(); + return $refcount ? $refcount -> [0] : $self -> self_error("Unable to locate entity ".$self -> {"entity_table"}."[$id]: does not exist"); +} + + +## @method protected $ _update_refcount($id, %operation) +# Update the refcount for the specified entity. This will increment, decrement, +# or set the value stored in the specified entity's reference counter. This is the +# actual implementation underlying increase_refcount() and decrease_refcount(). +# +# @param id The ID of the entity to update the refcount for. +# @param operation A hash containing one of 'add', 'subtract', or 'set' with +# values that indicate how much to change the refcount by. +# @return The new value of the reference count on success (which may be zero), undef on error. +sub _update_refcount { + my $self = shift; + my $id = shift; + my %operation = @_; + + $self -> clear_error(); + + my $refcount = $self -> _fetch_refcount($id); + return undef if(!defined($refcount)); + + # Calculate the new refcount + if(defined($operation{"add"})) { + $refcount += $operation{"add"}; + } elsif(defined($operation{"subtract"})) { + $refcount -= $operation{"subtract"}; + } elsif(defined($operation{"set"})) { + $refcount = $operation{"set"}; + } else { + return $self -> self_error("No valid operation specified in call to _update_refcount() for ".$self -> {"entity_table"}."[$id]"); + } + + # Is the new refcount sane? + return $self -> self_error("New refount of $refcount for entity ".$self -> {"entity_table"}."[$id]: is invalid") + if($refcount < 0 || ($self -> {"max_refcount"} && $refcount > $self -> {"max_refcount"})); + + # Update is safe, do the operation. + my $atth = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {$self -> {"entity_table"}}."` + SET `refcount` = ? + WHERE `id` = ?"); + my $result = $atth -> execute($refcount, $id); + return $self -> self_error("Unable to update entity refcount: ".$self -> {"dbh"} -> errstr) if(!$result); + return $self -> self_error("Entity refcount update failed: no rows updated. This should not happen!") if($result eq "0E0"); + + return $refcount; +} + +1; diff --git a/modules/ORB/System/Metadata.pm b/modules/ORB/System/Metadata.pm new file mode 100755 index 0000000..371c855 --- /dev/null +++ b/modules/ORB/System/Metadata.pm @@ -0,0 +1,369 @@ +## @file +# This file contains the implementation of the metadata handling engine. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# A class to encapsulate metadata context handling. This class provides the +# methods required to manage metadata contexts in the system, including +# creating and removing them, and modifying the reference count in the context +# when roles, tags, courses, resources and so on are added to, or removed from, +# the context. +# +# Metadata contexts are generic containers to which other pieces of data may +# be attached. Usually each context has a parent, and if the parent is undef +# the context is considered to be a root context. Generally there will only be +# a single root in the system - the one corresponding to the front page of the +# site. Contexts also have a reference count, which keeps track of how many +# things have attached themselves to the context - a metadata context can not +# be deleted unless its reference count is zero. +# +# Individual things - roles, tags, etc - need to keep track of which metadata +# context they are attached to, by storing a metadata ID with their data. The +# metadata context itself does not retain a list of attached 'things'. +package ORB::System::Metadata; + +use strict; +use parent qw(Webperl::SystemModule); + +# ============================================================================ +# Clean shutdown support + +## @method void clear() +# A function callable by System to ensure that the 'ondestroy' array does not +# prevent object destruction. +sub clear { + my $self = shift; + + # Nuke any ondelete entries + $self -> {"ondestroy"} = []; +} + + +# ============================================================================== +# Public interface + +## @method void register_ondestroy($obj) +# Register a class as needing to have its on_metadata_destroy() function called +# when destroy() removes a metadata context. +# +# @param obj A reference to an object that needs to do cleanup when a context is destroyed. +sub register_ondestroy { + my $self = shift; + my $obj = shift; + + push(@{$self -> {"ondestroy"}}, $obj); +} + + +## @method $ create($parentid) +# Create a new metadata context with the specified parent ID, and return the ID +# of the newly created context. +# +# @param parentid The ID of this context's parent, or undef to create a root. Note +# that root contexts should only be created with the utmost caution, +# as they terminate role inheritance hierarchies. +# @return The new metadata context id, or undef on error. +sub create { + my $self = shift; + my $parentid = shift; + + $self -> clear_error(); + + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + (parent_id) + VALUES(?)"); + my $rows = $newh -> execute($parentid); + return $self -> self_error("Unable to perform metadata insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Metadata insert failed, no rows inserted") if($rows eq "0E0"); + + # FIXME: This ties to MySQL, but is more reliable that last_insert_id in general. + # Try to find a decent solution for this mess... + # NOTE: the DBD::mysql documentation doesn't actually provide any useful information + # about what this will contain if the insert fails. In fact, DBD::mysql calls + # libmysql's mysql_insert_id(), which returns 0 on error (last insert failed). + # There, why couldn't they bloody /say/ that?! + my $metadataid = $self -> {"dbh"} -> {"mysql_insertid"}; + + # Increment the parent's refcount if addition worked + $self -> attach($parentid) or return undef + if($parentid && $metadataid); + + # Return undef if metadataid is undef or zero + return $metadataid ? $metadataid : undef; +} + + +## @method $ destroy($metadataid) +# Attempt to delete the specified metadata context from the system. If the refcount for +# the context is non-zero, this will fail with an error. Note that this will not attempt +# to delete any child contexts - they must have been deleted, and detached, before calling +# this function. +# +# @param metadataid The ID of the metadata context to delete. +# @return true on success, undef on error. +sub destroy { + my $self = shift; + my $metadataid = shift; + + $self -> clear_error(); + + # Can't do anything without knowing the reference count value + my $refcount = $self -> _fetch_metadata_refcount($metadataid); + return undef if(!defined($refcount)); + + return $self -> self_error("Attempt to delete metadata context $metadataid with non-zero ($refcount) reference count.") + if($refcount); + + # Reference count is zero, so nuke the context + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + WHERE id = ?"); + $nukeh -> execute($metadataid) + or return $self -> self_error("Metadata context delete failed: ".$self -> {"dbh"} -> errstr); + + # Call any objects that need to do cleanup + foreach my $obj (@{$self -> {"ondestroy"}}) { + $obj -> on_metadata_destroy($metadataid) if($obj -> can("on_metadata_destroy")); + } + + return 1; +} + + +## @method $ attach($metadataid) +# Attach to the specified metadata context. This increments the specified metadata +# context's reference counter, ensuring that it can not be destroyed until everything +# that references it has detached. +# +# @param metadataid The ID of the metadata context to attach to. +# @return The number of references on success, undef on error. +sub attach { + my $self = shift; + my $metadataid = shift; + + return $self -> _update_metadata_refcount($metadataid, 1); +} + + +## @method $ detach($metadataid, $retain_unused) +# Detach from the specified metadata context. This decrements the specified metadata +# context's reference counter, potentially zeroing it - if this happens, and +# retain_unused has not been set, the metadata context will be deleted. +# +# @param metadataid The ID of the metadata context to attach to. +# @param retain_unused If true, do not delete the context even if its refcount will +# be zero after calling this. +# @return The reference count (which may be zero) on success, false on error. +sub detach { + my $self = shift; + my $metadataid = shift; + my $retain_unused = shift; + + # Change the refcount, bomb if the change failed. + my $result = $self -> _update_metadata_refcount($metadataid, 0); + return undef if(!defined($result)); + + # Nuke the context if there's no reason to keep it around. + return $self -> destroy($metadataid) + unless($retain_unused || $result); + + return $result; +} + + +## @method $ parentid($metadataid) +# Obtain the ID of the specified metadata context's parent. This will look up the +# ID of the parent of the specified metadata context, and return it, potentially +# returning undef if the parent id is NULL - the specified metadata id corresponds +# to the root context. This method will attempt to use the role cache before going +# to the database for the id. +# +# @param metadataid The ID of the metadata context to obtain the parent ID for. +# @return The ID of the metadata context's parent (which may be ""). If an error +# occurred, this will return undef and the error message will be stored +# in $self -> {"errstr"} +sub parentid { + my $self = shift; + my $metadataid = shift; + + # Check the cache for the metadata entry, return the parent if it's there... + return $self -> {"cache"} -> {"metadata"} -> {$metadataid} -> {"parent_id"} + if(defined($self -> {"cache"} -> {"metadata"} -> {$metadataid} -> {"parent_id"})); + + # Otherwise, fetch the parent + my $parentid = $self -> _fetch_metadata_parentid($metadataid); + return undef if(!defined($parentid)); + + # Cache the parent + $self -> {"cache"} -> {"metadata"} -> {$metadataid} -> {"parent_id"} = $parentid; + + return $parentid; +} + + +## @method $ reparent($metadataid, $newparentid) +# Detatch the specified metadata context from its current parent (if possible) and +# reattach it to a different parent. This will handle ensuring that reference +# counters are changed appropriately, and links are maintained. +# +# @param metadataid The ID of the metadata context to reparent. +# @param newparentid The ID of the metadata context to set as the new parent. +# @return true on success, undef on error. +sub reparent { + my $self = shift; + my $metadataid = shift; + my $newparentid = shift; + + my $parentid = $self -> parentid($metadataid); + return undef if(!defined($parentid)); + + my $refcount = $self -> detach($parentid); + return undef if(!defined($refcount)); + + $self -> _set_metadata_parentid($metadataid, $newparentid) + or return undef; + + $self -> attach($newparentid) + or return undef; + + return 1; +} + + +# ============================================================================== +# Private methods + + +## @method private $ _fetch_metadata_parentid($metadataid) +# Obtain the metadata ID of the parent of the specified metadata context. If the +# metadataid specified corresponds to a root node (one with no parent), this will +# return an empty string. +# +# @param metadataid The ID of the metadata context to find the parent ID for. +# @return The metadata context's parent ID, the empty string if the context has no +# parent, or undef on error. +sub _fetch_metadata_parentid { + my $self = shift; + my $metadataid = shift; + + $self -> clear_error(); + + my $parenth = $self -> {"dbh"} -> prepare("SELECT parent_id FROM ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + WHERE id = ?"); + $parenth -> execute($metadataid) + or return $self -> self_error("Unable to perform metadata parent lookup: ". $self -> {"dbh"} -> errstr); + + # This should return something, or the metadataid is invalid + my $parent = $parenth -> fetchrow_arrayref(); + return $self -> self_error("Unable to find metadata parent: invalid metadataid specified") if(!$parent); + + # Parent is either defined, or undef. If it's undef, return "" instead. + return $parent -> [0] || ""; +} + + +## @method private $ _fetch_metadata_refcount($metadataid) +# Obtain the reference count for the specified metadata context. +# +# @param metadataid The ID of the metadata context to fetch the refcount for. +# @return The reference count, which may be zero, or undef on error. +sub _fetch_metadata_refcount { + my $self = shift; + my $metadataid = shift; + + $self -> clear_error(); + + # Check that the refcount exists, and under/overflow is not going to happen + my $checkh = $self -> {"dbh"} -> prepare("SELECT refcount FROM ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + WHERE id = ?"); + $checkh -> execute($metadataid) + or return $self -> self_error("Metadata lookup failed: ".$self -> {"dbh"} -> errstr); + + my $metadata = $checkh -> fetchrow_arrayref(); + return $self -> self_error("Metadata refcount update failed: unable to locate context $metadataid") + if(!$metadata); + + return $metadata -> [0]; +} + + +## @method private $ _update_metadata_refcount($metadataid, $increment) +# Increment or decrement the value stored in the specified metadata context's reference +# counter. This is the actual implementation underlying attach() and detach(). +# +# @param metadataid The ID of the metadata context to update the refcount for. +# @param increment If true, the refcount is incremented, otherwise it is decremented. +# @return The new value of the reference count on success (which may be zero), undef on error. +sub _update_metadata_refcount { + my $self = shift; + my $metadataid = shift; + my $increment = shift; + + $self -> clear_error(); + + my $refcount = $self -> _fetch_metadata_refcount($metadataid); + return undef if(!defined($refcount)); + + return $self -> self_error("Metadata refcount update failed: attempt to set refcount for $metadataid out of range (old: $refcount, mode is ".($increment ? "inc)" : "dec)")) + if((!$increment && $refcount == 0) || ($increment && $refcount == $self -> {"max_refcount"})); + + # Update is safe, do the operation. + my $atth = $self -> {"dbh"} -> prepare("UPDATE ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + SET refcount = refcount ".($increment ? "+" : "-")." 1 + WHERE id = ?"); + my $result = $atth -> execute($metadataid); + + # Detect and handle errors + return $self -> self_error("Unable to update metadata refcount: ".$self -> {"dbh"} -> errstr) + if(!$result); + + # Detect row change failure, assume bad id + return $self -> self_error("Metadata refcount update failed: no rows updated. This should not happen!") + if($result eq "0E0"); + + # Work out what the new refcount is and return it + $refcount = ($increment ? $refcount + 1 : $refcount - 1); + return $refcount; +} + + +## @method private $ _set_metadata_parentid($metadataid, $parentid) +# Set the parent ID of the specified metadata context. +# +# @param metadataid The ID of the metadata context to set the parent for. +# @param parentid The ID of the metadata context's new parent. +# @return true on success, undef on error. +sub _set_metadata_parentid { + my $self = shift; + my $metadataid = shift; + my $parentid = shift; + + my $seth = $self -> {"dbh"} -> prepare("UPDATE ".$self -> {"settings"} -> {"database"} -> {"metadata"}." + SET parent_id = ? + WHERE id = ?"); + my $result = $seth -> execute($parentid, $metadataid); + + # Detect and handle errors + return $self -> self_error("Unable to update metadata parent: ".$self -> {"dbh"} -> errstr) if(!$result); + return $self -> self_error("Metadata parent update failed: no rows updated. This should not happen!") if($result eq "0E0"); + + # Cache the parent + $self -> {"cache"} -> {"metadata"} -> {$metadataid} -> {"parent_id"} = $parentid; + + return 1; +} + +1; diff --git a/modules/ORB/System/Roles.pm b/modules/ORB/System/Roles.pm new file mode 100755 index 0000000..f673bbb --- /dev/null +++ b/modules/ORB/System/Roles.pm @@ -0,0 +1,883 @@ +## @file +# This file contains the implementation of the Role handling engine. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# This class encapsulates operations involving roles in the system. The methods +# in this class provide the rest of the system with the ability to query user's +# roles and capabilities, as well as assign roles to users, remove those assignments, +# and define the capabilities of roles. +# +package ORB::System::Roles; + +use strict; +use parent qw(Webperl::SystemModule); + +# ============================================================================== +# Creation + +## @cmethod $ new(%args) +# Create a new Roles object to manage user role allocation and lookup. +# The minimum values you need to provide are: +# +# * dbh - The database handle to use for queries. +# * settings - The system settings object +# * metadata - The system Metadata object. +# * logger - The system logger object. +# +# @param args A hash of key value pairs to initialise the object with. +# @return A new Roles object, or undef if a problem occured. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = $class -> SUPER::new(root_context => 1, + @_) + or return undef; + + # Check that the required objects are present + return Webperl::SystemModule::set_error("No metadata object available.") if(!$self -> {"metadata"}); + + return $self; +} + + +# ============================================================================== +# Public interface - user-centric role functions. + +## @method $ user_capabilities($metadataid, $userid, $rolelimit) +# Obtain a hash of user capabilities for the specified metadata context. This will +# check the specified metadata context and all its parents to create a full +# set of capabilities the user has in the context.Normal role combination rules apply +# (higher priority roles will take precedent over lower priority roles), except +# that all metadata levels from the specified level to the root are considered. +# If `rolelimit` is specified, only roleids present in the hash will be included +# when determining whether the user has the requested capability. +# +# @param metadataid The ID of the metadata context to start searching from. This +# will generally be the context associated with the resource +# checking the user's capabilities. +# @param userid The ID of the user to establish the capabilities of. +# @param rolelimit An optional hash containing role ids as keys, and true or +# false as values. If a role id's value is true, the role +# will be allowed through to capability testing, otherwise +# the role is excluded from capability testing. IMPORTANT: +# specifying this hash *does not* grant the user any roles they +# do not already have - this is used to conditionally exclude +# roles the user *does* have from capability testing, not grant +# roles! +# @return A reference to a capability hash on success, undef on error. +sub user_capabilities { + my $self = shift; + my $metadataid = shift; + my $userid = shift; + my $rolelimit = shift; + + $self -> clear_error(); + + # User roles will accumulate in here... + my $user_roles = {}; + + # Now get all the roles the user has from this metadata context to the root + while($metadataid) { + # Fetch the roles for the user set at this metadata level, give up if there was + # an error doing it. + my $roles = $self -> metadata_assigned_roles($metadataid, $userid); + return undef if(!defined($roles)); + + # copy over any roles set... + foreach my $role (keys(%{$roles})) { + # skip any roles not present in $rolelimit, if needed + next if($rolelimit && !$rolelimit -> {$role}); + + $user_roles -> {$role} = $roles -> {$role}; + } + + # go up a level if possible + $metadataid = $self -> {"metadata"} -> parentid($metadataid); + } + + # $user_roles now contains an unsorted hash of roleids and priorities, + # we need a sorted list of roleids, highest priority last so that higher + # priorities overwrite lower + my @roleids = sort { $user_roles -> {$a} <=> $user_roles -> {$b} } keys(%{$user_roles}); + + my $capabilities = {}; + # Now fo through each role, merging the role capabilities into the + # capabilities hash + foreach my $roleid (@roleids) { + my $rolecaps = $self -> role_get_capabilities($roleid); + + # Merge the new capabilities into the ones collected so far, this will + # overwrite any capabilities already defined - hence the need to sort above! + @{$capabilities}{keys %{$rolecaps}} = values %{$rolecaps}; + } + + return $capabilities; +} + + +## @method $ user_has_capability($metadataid, $userid, $capability, $rolelimit) +# Determine whether the user has the specified capability within the metadata +# context. This will search the current metadata context, and its parents, to +# determine whether the user has the capability requested, and if so whether +# that capability is enabled or disabled. Normal role combination rules apply +# (higher priority roles will take precedent over lower priority roles), except +# that all metadata levels from the specified level to the root are considered. +# If `rolelimit` is specified, only roleids present in the hash will be included +# when determining whether the user has the requested capability. +# +# @param metadataid The ID of the metadata context to start searching from. This +# will generally be the context associated with the resource +# checking the user's capabilities. +# @param userid The ID of the user to establish the capabilities of. +# @param capability The name of the capability the user needs. +# @param rolelimit An optional hash containing role ids as keys, and true or +# false as values. If a role id's value is true, the role +# will be allowed through to capability testing, otherwise +# the role is excluded from capability testing. IMPORTANT: +# specifying this hash *does not* grant the user any roles they +# do not already have - this is used to conditionally exclude +# roles the user *does* have from capability testing, not grant +# roles! +# @return true if the user has the requested capability, false if they do not, undef +# if an error was encountered. +sub user_has_capability { + my $self = shift; + my $metadataid = shift; + my $userid = shift; + my $capability = shift; + my $rolelimit = shift; + + $self -> clear_error(); + + # Has the user's capability at this metadata level been queried before? If so, use the cached value. + # In most cases this will probably miss, but it's not like it is a big overhead. + return $self -> {"cache"} -> {"user"} -> {$userid} -> {"capabilities"} -> {$metadataid} -> {$capability} + if(defined($self -> {"cache"} -> {"user"} -> {$userid} -> {"capabilities"} -> {$metadataid} -> {$capability})); + + # User roles will accumulate in here... + my $user_roles = {}; + + # Need to preserve the original ID for caching, so copy it... + my $currentmdid = $metadataid; + + # Now get all the roles the user has from this metadata context to the root + while($currentmdid) { + # Fetch the roles for the user set at this metadata level, give up if there was + # an error doing it. + my $roles = $self -> metadata_assigned_roles($currentmdid, $userid); + return undef if(!defined($roles)); + + # copy over any roles set... + foreach my $role (keys(%{$roles})) { + # skip any roles not present in $rolelimit, if needed + next if($rolelimit && !$rolelimit -> {$role}); + + $user_roles -> {$role} = $roles -> {$role}; + } + + # FUTURE: At this point, discontinuities could be introduced into the role + # inheritance hierarchy by halting tree climbing if the metadata indicates + # inheritance should break here. However, doing so would probably need special + # edge-cases for admin users. + + # go up a level if possible + $currentmdid = $self -> {"metadata"} -> parentid($currentmdid); + } + + # $user_roles now contains an unsorted hash of roleids and priorities, + # we need a sorted list of roleids, highest priority first, to check for + # the capability. + my @roleids = sort { $user_roles -> {$b} <=> $user_roles -> {$a} } keys(%{$user_roles}); + + # Go through the list of roles, determining whether the role sets the + # capability in some fasion + my $set_capability; + foreach my $role (@roleids) { + $set_capability = $self -> role_has_capability($role, $capability); + return undef if(defined($set_capability) && $set_capability eq "error"); + + # stop if a setting for the capability has been located + last if($set_capability); + } + + # If set_capability is still undefined, none of the roles defined the capability, + # so set it to the default 'deny' + $set_capability = "deny" unless(defined($set_capability)); + + # store the result in the cache + $self -> {"cache"} -> {"user"} -> {$userid} -> {"capabilities"} -> {$metadataid} -> {$capability} = ($set_capability eq "allow"); + + # And done + return $self -> {"cache"} -> {"user"} -> {$userid} -> {"capabilities"} -> {$metadataid} -> {$capability}; +} + + +## @method $ user_has_role($metadataid, $userid, $roleid, $sourceid, $check_tree) +# Determine whether the user has the specified role in the current metadata context. +# If sourceid is specified, only roles granted by the corresponding enrolment source +# will be considered when checking for the role. If $check_tree is set, this will +# not only check the specified metadata context for the role, but any parent context, +# until either the role is found or the search fails at the root. Byt default, only +# the specified metadata context is checked. +# +# @param metadataid The ID of the metadata context to check. +# @param userid The ID of the user to check the role for. +# @param roleid The ID of the role to look for. +# @param sourceid If specified, only roles granted by this enrolment source are considered. +# @param check_tree If true, this function will walk back up the tree trying to locate +# any assignment of the role. Otherwise, only the specified context is +# checked. +# @return true if the user has the role, false if the user does not, and undef on error. +sub user_has_role { + my $self = shift; + my ($metadataid, $userid, $roleid, $sourceid, $check_tree) = @_; + + $self -> clear_error(); + + # Note lack of caching - while it would be possible to cache the result of this, doing + # so is likely to introduce subtle bugs. Caching can be added in future if needed. + my $has_role; + do { + $has_role = $self -> _fetch_user_role($metadataid, $userid, $roleid, $sourceid); + # _fetch_user_role returns undef when the role hasn't been granted, or error. Check errors... + return undef if($self -> {"errstr"}); + + # Otherwise, try going up to the parent + $metadataid = $self -> {"metadata"} -> parentid($metadataid); + } while(!$has_role && $check_tree && $metadataid); + + return defined($has_role); +} + + +## @method $ user_assign_role($metadataid, $userid, $roleid, $sourceid, $groupid) +# Assign the user a role in the specified metadata context. This will give the user +# the role, if the user does not already have it. If the user has the role, this +# will update the persist flag if it differs. +# +# @note Users may be given the same role by different enrolment sources, and the +# persist flag may be different for each source. This means that the user +# may have Role A set by Source A, and Role A set by Source B. In practice +# this feature is unlikely to be needed or used, but is provided Just In Case. +# +# @param metadataid The ID of the metadata context to grant the role in. +# @param userid The ID of the user to grant the role to. +# @param roleid The ID of the role to grant. +# @param sourceid The ID of the enrolment source granting the role. +# @param groupid The ID of the group this is an assignment for. If this is an +# individual assignment rather than a group assignment, set this +# to undef. +# @return true on success, false if an error occurred. +sub user_assign_role { + my $self = shift; + my ($metadataid, $userid, $roleid, $sourceid, $groupid) = @_; + + $self -> clear_error(); + + # Try to get the role, bomb if an error occurred + my $role = $self -> _fetch_user_role($metadataid, $userid, $roleid, $sourceid, $groupid); + return undef if(!$role && $self -> {"errstr"}); + + my $rows; + # no role found? Try to assign it. + if(!$role) { + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." + (metadata_id, role_id, user_id, source_id, group_id, attached, touched) + VALUES(?, ?, ?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"); + $rows = $newh -> execute($metadataid, $roleid, $userid, $sourceid, $groupid); + return $self -> self_error("Unable to perform metadata role insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + + # If a row has been added, increment the metadata's refcount + $rows = $self -> {"metadata"} -> attach($metadataid) + if($rows ne "0E0"); + + # A role has been located in this context that matches the user and source, + # update its touched timestamp + } else { + my $oldh = $self -> {"dbh"} -> prepare("UPDATE ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." + SET touched = UNIX_TIMESTAMP(), + WHERE id = ?"); + $rows = $oldh -> execute($role -> {"id"}); + return $self -> self_error("Unable to perform metadata role update: ". $self -> {"dbh"} -> errstr) if(!$rows); + } + + # Check that a row has been modified before finishing. + return $self -> self_error("Role assignment failed: no rows modified") if($rows eq "0E0"); + + return 1; +} + + +## @method $ user_remove_role($metadataid, $userid, $roleid, $sourceid, $groupid) +# Remove a role from a user in the specified metadata context. If the user does +# not have the role in the context, this does nothing, and this function *will not* +# traverse the tree looking for any assignment of the role to the user: it will +# inspect the specified metadata context only. If sourceid is not specified, the +# role is guaranteed to be entirely removed from the user in the context if it was +# set there. If sourceid is provided, only the role allocation previously made by +# the specified source will be removed, and any other copies of the role allocation +# made by other sources will remain in effect. +# +# @param metadataid The ID of the metadata context to remove the role in. +# @param userid The ID of the user to remove the role from. +# @param roleid The ID of the role to remove. +# @param sourceid The ID of the enrolment source removing the role, or undef if +# copies of the role applied by all sources should be removed. +# @param groupid The ID of the group this is a removal for. +# @return true on success, false if an error occurred. +sub user_remove_role { + my $self = shift; + my ($metadataid, $userid, $roleid, $sourceid, $groupid) = @_; + + $self -> clear_error(); + + my @args = ($metadataid, $roleid, $userid, $groupid); + my $query = "DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." + WHERE metadata_id = ? + AND role_id = ? + AND user_id = ? + AND group_id = ?"; + + if($sourceid) { + $query .= " AND source_id = ?"; + push(@args, $sourceid); + } + + my $nukeh = $self -> {"dbh"} -> prepare($query); + my $rows = $nukeh -> execute(@args); + + return $self -> self_error("Unable to perform metadata role delete: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Metadata context delete failed: no rows removed") if($rows eq "0E0"); + + # Removal worked, so decrement the refcount + my $refcount = $self -> {"metadata"} -> detach($metadataid); + return defined($refcount); +} + + +## @method $ get_role_users($metadataid, $roleid, $fields, $orderby, $sourceid) +# Obtain a list of users who have the specified role at this metadata level. If +# the sourceid is specified, only roles allocated by the specified source are +# considered when constructing the list. +# +# @param metadataid The ID of the metadata context to list users from. +# @param roleid The ID of the role that has been assigned to users. +# @param fields A reference to an array containing the user table fields to include +# in the returned data. If this is undef, all fields are collected. +# If the selected fields do not start with "u." it will be prepended +# for you. +# @param orderby Optional contents of the 'ORDER BY' clause of the query. The user table +# is aliased as 'u', so you can do things like "u.username ASC". Set +# to undef if you don't care about sorting. +# @param sourceid The ID of the enrolment source that allocated the role, or +# undef if any source is acceptable +# @return A reference to an array of hashrefs, each hashref contains the data +# for a user with the specified role. +sub get_role_users { + my $self = shift; + my $metadataid = shift; + my $roleid = shift; + my $fields = shift || []; + my $orderby = shift; + my $sourceid = shift; + + $self -> clear_error(); + + my $selectedfields = ""; + foreach my $field (@{$fields}) { + $selectedfields .= ", " if($selectedfields); + + # Make sure the field is coming out of the user table + $field = "u.$field" unless($field =~ /^u\./); + + $selectedfields .= $field; + } + # Fall back on fetching everything if there's no field selection. + $selectedfields = "u.*" if(!$selectedfields); + + # Simple query is pretty simple... + my $userh = $self -> {"dbh"} -> prepare("SELECT $selectedfields + FROM ".$self -> {"settings"} -> {"database"} -> {"users"}." AS u, + ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." AS r + WHERE r.role_id = ? + AND u.user_id = r.user_id" + .($sourceid ? " AND source_id = ?" : "") + .($orderby ? " ORDER BY $orderby" : "")); + if($sourceid) { + $userh -> execute($roleid, $sourceid) + or return $self -> self_error("Unable to fetch role users: ". $self -> {"dbh"} -> errstr); + } else { + $userh -> execute($roleid) + or return $self -> self_error("Unable to fetch role users: ". $self -> {"dbh"} -> errstr); + } + + # Fetch all the results as an array of hashrefs + return $userh -> fetchall_arrayref({}); +} + + +# ============================================================================== +# Public interface - role-centric role functions. + +## @method $ create($name, $priority, $capabilities) +# Create a new role, initialising it with the specified priority and capabilities. +# +# @param name The name of the role. If a role already exists with this name, +# the function will abort. +# @param priority The role priority, in the range 0 (lowest) to 255 (highest). +# @param capabilities A reference to a hash containing the capabilities to set for +# the role. Keys should be capability names, and the values should +# be true for 'allow' and false for 'deny'. Set this to undef to +# skip capability settings. +# @return The new role ID on success, undef on error. +sub create { + my $self = shift; + my $name = shift; + my $priority = shift; + my $capabilities = shift; + + $self -> clear_error(); + + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"roles"}." + (name, priority) + VALUES(?, ?)"); + my $rows = $newh -> execute($name, $priority); + return $self -> self_error("Unable to perform role insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Role insert failed, no rows inserted") if($rows eq "0E0"); + + # FIXME: This ties to MySQL, but is more reliable that last_insert_id in general. + # Try to find a decent solution for this mess... + # NOTE: the DBD::mysql documentation doesn't actually provide any useful information + # about what this will contain if the insert fails. In fact, DBD::mysql calls + # libmysql's mysql_insert_id(), which returns 0 on error (last insert failed). + # There, why couldn't they bloody /say/ that?! + my $roleid = $self -> {"dbh"} -> {"mysql_insertid"}; + return $self -> self_error("Unable to obtain roleid for role '$name'") if(!$roleid); + + # Skip capability setting if there are no capabilities to set. + return $roleid if(!$capabilities); + + # Set up the capabilities + return $self -> role_set_capabilities($roleid, $capabilities) ? $roleid : undef; +} + + +## @method $ destroy($roleid) +# Attempt to remove the specified role, and any capabilities associated with it, from +# the system. +# +# @warning This will remove the role, the capabilities set for the role, and any +# role assignments. It will work even if there are users currently allocated +# this role. Use with extreme caution! +# +# @param roleid The ID of the role to remove from the system +# @return true on success, undef on error +sub destroy { + my $self = shift; + my $roleid = shift; + + $self -> clear_error(); + + # Delete any role assignments first. This is utterly indiscriminate, if this breaks + # something important, don't say I didn't warn you. + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." + WHERE role_id = ?"); + $nukeh -> execute($roleid) + or return $self -> self_error("Unable to perform role allocation removal: ". $self -> {"dbh"} -> errstr); + + # Now delete the capabilities + $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"role_capabilities"}." + WHERE role_id = ?"); + $nukeh -> execute($roleid) + or return $self -> self_error("Unable to perform role capability removal: ". $self -> {"dbh"} -> errstr); + + # And now the role itself + $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"roles"}." + WHERE id = ?"); + $nukeh -> execute($roleid) + or return $self -> self_error("Unable to perform role removal: ". $self -> {"dbh"} -> errstr); + + # Trash the cache, too, just in case + $self -> {"cache"} = {}; + + return 1; +} + + +## @method $ set_priority($roleid, $priority) +# Update the priority for the specified role. +# +# @param roleid The role to update the priority for. +# @param priority The new priority for the role. +# @return true on success, undef on error. +sub set_priority { + my $self = shift; + my $roleid = shift; + my $priority = shift; + + $self -> clear_error(); + + my $seth = $self -> {"dbh"} -> prepare("UPDATE ".$self -> {"settings"} -> {"database"} -> {"roles"}." + SET priority = ? + WHERE id = ?"); + my $rows = $seth -> execute($priority, $roleid); + return $self -> self_error("Unable to perform role priority update: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Role priority update failed: no rows updated (possibly bad role id $roleid)") if($rows eq "0E0"); + + # This could invalidate the cache, so clear it + $self -> {"cache"} = {}; + + return 1; +} + + +## @method $ get_priority($roleid) +# Obtain the priority of the specified role. +# +# @param roleid The role to obtain the priority for. +# @return The priority of the role on success (note this may be zero!), undef on error. +sub get_priority { + my $self = shift; + my $roleid = shift; + + $self -> clear_error(); + + my $geth = $self -> {"dbh"} -> prepare("SELECT priority FROM ".$self -> {"settings"} -> {"database"} -> {"roles"}." + WHERE id = ?"); + $geth -> execute($roleid) + or return $self -> self_error("Unable to perform role priority lookup: ". $self -> {"dbh"} -> errstr); + + my $role = $geth -> fetchrow_arrayref(); + + return $role ? $role -> [0] : $self -> self_error("Role priority lookup failed: bad role id $roleid"); +} + + +## @method $ role_get_roleid($name) +# Given a role name, obtain the id of the role. +# +# @param name The name of the role to look up. +# @return The id of the role, or undef if the role name is not valid or an error +# occurred. +sub role_get_roleid { + my $self = shift; + my $name = shift; + + $self -> clear_error(); + + my $roleh = $self -> {"dbh"} -> prepare("SELECT id FROM ".$self -> {"settings"} -> {"database"} -> {"roles"}." + WHERE role_name LIKE ?"); + $roleh -> execute($name) + or return $self -> self_error("Unable to perform role lookup: ". $self -> {"dbh"} -> errstr); + + # Return the role id if it is found, otherwise undef. + my $role = $roleh -> fetchrow_arrayref(); + return $role ? $role -> [0] : undef; +} + + +## @method $ role_has_capability($roleid, $capability) +# Determine whether the specified role defines the requested capability. This will +# check whether the capability is defined for the role, and if it is return the +# value that is set for its mode. If the capability is not set for the role, +# this returns undef. +# +# @param roleid The ID of the role to check. +# @param capability The name of the capability to look for in the role. +# @return 'allow' or 'deny' if the role sets the capability, undef otherwise. +# This will return 'error' if an error was encountered. +sub role_has_capability { + my $self = shift; + my $roleid = shift; + my $capability = shift; + + # Is the value cached? + if($self -> {"cache"} -> {"role"} -> {$roleid} -> {$capability}) { + # If the cache indicates the value is undefined for this role, return undef, otherwise return + # the cached value. + return undef if($self -> {"cache"} -> {"role"} -> {$roleid} -> {$capability} eq "undef"); + return $self -> {"cache"} -> {"role"} -> {$roleid} -> {$capability}; + } + + # Ask the database for the capability definition, if provided + my $set_capability = $self -> _fetch_role_capability($roleid, $capability); + return "error" if(defined($set_capability) && $set_capability eq "error"); + + # Cache the result + $self -> {"cache"} -> {"role"} -> {$roleid} -> {$capability} = defined($set_capability) ? $set_capability : "undef"; + + return $set_capability; +} + + +## @method $ role_get_capabilities($roleid) +# Obtain a hash containing the capabilities defined for the specified role. This +# will pull the role capability settings out of the database and return a hash +# containing the capability names as keys and their mode as the value. The value +# is true if the mode is 'allow', and false if it is 'deny'. +# +# @param roleid The ID of the role to obtain capability data for. +# @return A reference to a hash containing role capabilities on success, undef +# on error. +sub role_get_capabilities { + my $self = shift; + my $roleid = shift; + + $self -> clear_error(); + + # There's no need to order the results of this, as it's going into a hash anyway + my $caph = $self -> {"dbh"} -> prepare("SELECT capability, mode + FROM ".$self -> {"settings"} -> {"database"} -> {"role_capabilities"}." + WHERE role_id = ?"); + $caph -> execute($roleid) + or return $self -> self_error("Unable to fetch role capability list: ".$self -> {"dbh"} -> errstr); + + # Bung the results in a hash. Would be nice to use fetchall here, but for the mode translate + my $caphash = {}; + while(my $capability = $caph -> fetchrow_hashref()) { + $caphash -> {$capability -> {"capability"}} = ($capability -> {"mode"} eq "allow"); + } + + return $caphash; +} + + +## @method $ role_set_capabilities($roleid, $capabilities) +# Update the capabilities for the specified role. This takes a hash of capabilities, +# capability names as keys and mode as value (true is 'allow', false is 'deny') and +# sets the capabilities for the role to the settings in the hash. Note that any +# capabilities not in the hash, but previously assigned to the role *will be removed*. +# +# @param roleid The ID of the role to update. +# @param capabilities A reference to a hash containing the capabilities to set for +# the role. Keys should be capability names, and the values should +# be true for 'allow' and false for 'deny'. +# @return true on success, undef on error. Note that if an error occurs, it is possible +# that the capability list may no longer reflect the old list, or the full +# list specified in the capabilities hash. +sub role_set_capabilities { + my $self = shift; + my $roleid = shift; + my $capabilities = shift; + + $self -> clear_error(); + + # Nuke any existing capabilities for this role + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"role_capabilities"}." + WHERE role_id = ?"); + $nukeh -> execute($roleid) + or return $self -> self_error("Unable to delete capabilities for role $roleid: ".$self -> {"dbh"} -> errstr); + + # Now insert new capabilities + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"role_capabilities"}." + (role_id, capability, mode) + VALUES(?, ?, ?)"); + foreach my $capability (keys(%{$capabilities})) { + my $inserted = $newh -> execute($roleid, $capability, $capabilities -> {$capability} ? "allow" : "deny"); + + return $self -> self_error("Unable to give capability $capability to role $roleid: ".$self -> {"dbh"} -> errstr) + if(!$inserted); + + return $self -> self_error("Unable to give capability $capability to role $roleid: row insertion failed") + if($inserted eq "0E0"); + } + + # Reset the cache, to prevent stale values causing problems + $self -> {"cache"} = {}; + + return 1; +} + + +# ============================================================================== +# Public-ish, mostly internal metadata bridge + +## @method $ metadata_assigned_roles($metadataid, $userid) +# Obtain a hash of roles defined for the user in the specified metadata context. This +# returns only the roles defined *in the specified metadata*, it does not include any +# roles defined in any parents. If no roles are defined at this level, an empty hash +# is returned, otherwise the returned hash contains the roleids as keys and their +# priorities as values. +# +# @param metadataid The ID of the metadata context to fetch user roles for. +# @param userid The ID of the user to fetch roles for. +# @return A hash containing the roles set at this level, role ids as keys and +# role priorities as values. If any errors are encountered, this returns +# undef and the error message is stored in $self -> {"errstr"} +sub metadata_assigned_roles { + my $self = shift; + my $metadataid = shift; + my $userid = shift; + + # Are the user's roles at this level cached? + return $self -> {"cache"} -> {"user"} -> {$userid} -> {"roles"} -> {$metadataid} + if(defined($self -> {"cache"} -> {"user"} -> {$userid} -> {"roles"} -> {$metadataid})); + + # Not cached, try to fetch them + my $roles = $self -> _fetch_metadata_roles($metadataid, $userid); + return undef if(!defined($roles)); + + $self -> {"cache"} -> {"user"} -> {$userid} -> {$metadataid} -> {"roles"} = $roles; + + return $roles; +} + + +# ============================================================================== +# Private methods + +## @method private $ _fetch_metadata_roles($metadataid, $userid) +# Fetch the roles set for the user at the specified metadata level. If there +# are no roles for the user attached to the specified metadata id, this returns +# an empty roles hash, otherwise it will return a hash containing the ids of +# any roles set and their priorities. +# +# @param metadataid The ID of the metadata context to fetch user roles for. +# @param userid The ID of the user to fetch roles for. +# @return A hash containing the roles set at this level, role ids as keys and +# role priorities as values. If any errors are encountered, this returns +# undef and the error message is stored in $self -> {"errstr"} +sub _fetch_metadata_roles { + my $self = shift; + my $metadataid = shift; + my $userid = shift; + my $set_roles = {}; + + $self -> clear_error(); + + # Pull the list of roles, highest priority first + my $roleh = $self -> {"dbh"} -> prepare("SELECT r.id, r.priority + FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." AS m, + ".$self -> {"settings"} -> {"database"} -> {"roles"}." AS r + WHERE r.id = m.role_id + AND m.metadata_id = ? + AND m.user_id = ? + ORDER BY r.priority DESC"); + $roleh -> execute($metadataid, $userid) + or return $self -> self_error("Unable to perform metadata role lookup: ". $self -> {"dbh"} -> errstr); + + # Store roles set at this level, and their priorities + while(my $role = $roleh -> fetchrow_hashref()) { + $set_roles -> {$role -> {"id"}} = $role -> {"priority"}; + } + + # If the user has no roles in this context, and default roles have been enabled, + # determine whether the context has a default role set. + $set_roles = $self -> _fetch_metadata_default_role($metadataid) + if(!scalar($set_roles) && $self -> {"settings"} -> {"database"} -> {"metadata_default_roles"}); + + return $set_roles; +} + + +## @method private $ _fetch_metadata_default_role($metadataid) +# Obtain the default role set for the specified metadata context, and its priority, if +# the metadata context has a default role set. +# +# @param metadataid The ID of the metadata context to obtain the default role for. +# @return A reference to a hash containing the default role id and priority on +# success, an empty hashref if no default is defined, and undef on error. +sub _fetch_metadata_default_role { + my $self = shift; + my $metadataid = shift; + + my $defh = $self -> {"dbh"} -> prepare("SELECT d.role_id,d.priority AS override,r.priority + FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_default_roles"}." AS d, + ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." AS r + WHERE d.metadata_id = ? + AND r.id = d.role_id"); + $defh -> execute($metadataid) + or return $self -> self_error("Unable to perform metadata default role lookup: ". $self -> {"dbh"} -> errstr); + + # If a default has been specified, return a hashref with it set, otherwise just return an empty hashref. + my $def = $defh -> fetchrow_hashref(); + return $def ? {$def -> {"role_id"} => ($def -> {"override"} || $def -> {"priority"})} : {}; +} + + +## @method private $ _fetch_role_capability($roleid, $capability) +# Look up whether the specified role defines the requested capability, and if +# it does, return the mode defined for it. +# +# @param roleid The ID of the role to check. +# @param capability The name of the capability to look for in the role. +# @return 'allow' or 'deny' if the role sets the capability, undef otherwise. +# This will return 'error' if an error was encountered. +sub _fetch_role_capability { + my $self = shift; + my $roleid = shift; + my $capability = shift; + + $self -> clear_error(); + + my $roleh = $self -> {"dbh"} -> prepare("SELECT mode FROM ".$self -> {"settings"} -> {"database"} -> {"role_capabilities"}." + WHERE role_id = ? + AND capability LIKE ?"); + $roleh -> execute($roleid, $capability) + or return ($self -> self_error("Unable to perform role capability lookup: ". $self -> {"dbh"} -> errstr) || "error"); + + # Fetch the role's definition of the capability, and it if has one return it. + my $role = $roleh -> fetchrow_arrayref(); + return $role ? $role -> [0] : undef; +} + + +## @method private $ _fetch_user_role($metadataid, $userid, $roleid, $sourceid, $groupid) +# Obtain the metadata role record for the user and role. If sourceid is specified, +# this will only return a metadata role record if it was granted by the source. If +# a group id is specified, this will only return a record if it was added as part of +# that group. +# +# @param metadataid The ID of the metadata context to look at for the role assignment. +# @param userid The ID of the user whose role allocation should be checked. +# @param roleid The ID of the role to look for. +# @param sourceid Optional ID of the enrolment source that granted the role. +# @param groupid Optional ID of a group the role assignment was made as part of. +# @return A reference to a hash containing the metadata role allocation, or undef +# if no matching allocation was found, or an error occurred. +sub _fetch_user_role { + my $self = shift; + my ($metadataid, $userid, $roleid, $sourceid, $groupid) = @_; + + $self -> clear_error(); + + my $query = "SELECT * + FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_roles"}." + WHERE metadata_id = ? + AND user_id = ? + AND role_id = ?"; + my @args = ($metadataid, $userid, $roleid); + + if($sourceid) { + $query .= " AND source_id = ?"; + push(@args, $sourceid); + } + + if($groupid) { + $query .= " AND group_id = ?"; + push(@args, $groupid); + } + + my $roleh = $self -> {"dbh"} -> prepare($query); + $roleh -> execute(@args) + or return $self -> self_error("Unable to perform metadata role lookup: ". $self -> {"dbh"} -> errstr); + + return $roleh -> fetchrow_hashref(); +} + +1; diff --git a/modules/ORB/System/Tags.pm b/modules/ORB/System/Tags.pm new file mode 100755 index 0000000..0615846 --- /dev/null +++ b/modules/ORB/System/Tags.pm @@ -0,0 +1,512 @@ +## @file +# This file contains the implementation of the tag handling engine. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class +# This class encapsulates operations involving tags in the system. +package ORB::System::Tags; + +use strict; +use parent qw(Webperl::SystemModule); + +# ============================================================================== +# Creation + +## @cmethod $ new(%args) +# Create a new Tags object to manage tag allocation and lookup. +# The minimum values you need to provide are: +# +# * dbh - The database handle to use for queries. +# * settings - The system settings object +# * metadata - The system Metadata object. +# * logger - The system logger object. +# +# @param args A hash of key value pairs to initialise the object with. +# @return A new Tags object, or undef if a problem occured. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = $class -> SUPER::new(@_) + or return undef; + + # Check that the required objects are present + return Webperl::SystemModule::set_error("No metadata object available.") if(!$self -> {"metadata"}); + + # Register with the metadata destroy handler + $self -> {"metadata"} -> register_ondestroy($self); + + return $self; +} + + +# ============================================================================ +# Public interface - tag creation, deletion, etc + +## @method $ create($name, $userid) +# Create a new tag with the specified name. This will create a new tag, setting +# its name and creator to the values specified. Note that this will not check +# whether a tag with the same name already exists +# +# @param name The name of the tag to add. +# @param userid The ID of the user creating the tag. +# @return The new tag ID on success, undef on error. +sub create { + my $self = shift; + my $name = shift; + my $userid = shift; + + $self -> clear_error(); + + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"tags"}." + (name, creator_id, created) + VALUES(?, ?, UNIX_TIMESTAMP())"); + my $rows = $newh -> execute($name, $userid); + return $self -> self_error("Unable to perform tag insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Tag insert failed, no rows inserted") if($rows eq "0E0"); + + # FIXME: This ties to MySQL, but is more reliable that last_insert_id in general. + # Try to find a decent solution for this mess... + # NOTE: the DBD::mysql documentation doesn't actually provide any useful information + # about what this will contain if the insert fails. In fact, DBD::mysql calls + # libmysql's mysql_insert_id(), which returns 0 on error (last insert failed). + # There, why couldn't they bloody /say/ that?! + my $tagid = $self -> {"dbh"} -> {"mysql_insertid"}; + return $self -> self_error("Unable to obtain id for tag '$name'") if(!$tagid); + + return $tagid; +} + + +## @method $ destroy($tagid) +# Attempt to remove the specified tag, and any assignments of it, from the system. +# +# @warning This will remove the tag, any tag assignments, and any active flags for the +# tag. It will work even if there are resources currently tagged with this tag. +# Use with extreme caution! +# +# @param tagid The ID of the tag to remove from the system +# @return true on success, undef on error +sub destroy { + my $self = shift; + my $tagid = shift; + + $self -> clear_error(); + + # Delete any tag assignments first. This is utterly indiscriminate, if this breaks + # something important, don't say I didn't warn you. + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + WHERE tag_id = ?"); + $nukeh -> execute($tagid) + or return $self -> self_error("Unable to perform tag allocation removal: ". $self -> {"dbh"} -> errstr); + + # Delete any activations of this tag + $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"active_tags"}." + WHERE tag_id = ?"); + $nukeh -> execute($tagid) + or return $self -> self_error("Unable to perform active tag removal: ". $self -> {"dbh"} -> errstr); + + # And now delete the tag itself + $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"tags"}." + WHERE id = ?"); + $nukeh -> execute($tagid) + or return $self -> self_error("Unable to perform tag removal: ". $self -> {"dbh"} -> errstr); + + return 1; +} + + +## @method $ get_tagid($name, $userid) +# Obtain the ID associated with the specified tag. If the tag does not yet exist +# in the tags table, this will create it and return the ID the new row was +# allocated. +# +# @param name The name of the tag to obtain the ID for +# @param userid The ID of the user requesting the tag, in case it must be created. +# @return The ID of the tag on success, undef on error. +sub get_tagid { + my $self = shift; + my $name = shift; + my $userid = shift; + + # Search for a tag with the specified name, give up if an error occurred + my $tagid = $self -> _fetch_tagid($name); + return $tagid if($tagid || $self -> {"errstr"}); + + # Get here and the tag doesn't exist, create it + return $self -> create($name, $userid); +} + + +## @method $ attach($metadataid, $tagid, $userid, $persist, $rating) +# Attach a tag to a metadata context. This will attempt to apply the specified tag +# to the metadata context, recording the user that requested the attachment. +# +# @param metadataid The ID of the metadata context to attach the tag to. +# @param tagid The ID of the tag to attach. +# @param userid The ID of the user responsible for attaching the tag. +# @param rating Optional initial rating to give the tag. If not specified, this +# will default to "default_rating" in the configuration. +# @return true on success, undef on error. +sub attach { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + my $rating = shift || $self -> {"settings"} -> {"config"} -> {"default_rating"} || 0; + + $self -> clear_error(); + + # determine whether the tag is already set on this metadata context + my $tag = $self -> _attached($metadataid, $tagid); + return undef if(!defined($tag)); + + # Tag is already set, return true, but log it as it shouldn't really happen + if($tag) { + $self -> {"logger"} -> log("warning", $userid, undef, "Attempt to re-set tag $tagid on metadata $metadataid by $userid."); + return 1; + } + + # Tag is not set, so add it + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + (metadata_id, tag_id, attached_by, attached_date, rating) + VALUES(?, ?, ?, UNIX_TIMESTAMP(), ?)"); + my $rows = $newh -> execute($metadataid, $tagid, $userid, $rating); + return $self -> self_error("Unable to perform metadata tag insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Metadata tag insert failed: no rows modified") if($rows eq "0E0"); + + # Tag has been set, what is the ID of the newly added metadata tag relation? + my $relation = $self -> {"dbh"} -> {"mysql_insertid"}; + return $self -> self_error("Unable to obtain id for metadata tag relation '$tagid' on '$metadataid'") if(!$relation); + + # Attach to the metadata context + my $attached = $self -> {"metadata"} -> attach($metadataid); + return undef if(!$attached); # this should always be 1 or greater if the attach worked. + + # And log the tagging operation in the history + return $self -> _log_action($metadataid, $tagid, "added", $userid, $rating); +} + + +## @method $ get_attached_tags($metadataid, $alphasort) +# Generate a list of tags attached to the specified metadata context. This will create +# a list containing reference to tag data hashes, and return a reference to it. If there +# are no tags attached to the context, this returns a reference to an empty list. +# +# @note This will not fetch tags attached to parent contexts: only tags attached to the +# current context are returned. If your code needs to inherit from the parent, you +# will need to call this on the parent context and merge the arrays yourself. +# +# @param metadataid The ID of the metadata context to fetch the tags for. +# @param alphasort If true, sort the list alphanumerically. If this is false, the list +# is sorted by rating (highest first), and then alphanumerically. +# @return A reference to an array of tag data hashes (which may be empty) on success, +# undef if an error occurred. +sub get_attached_tags { + my $self = shift; + my $metadataid = shift; + my $alphasort = shift; + + # Tag lookup query, pretty simple... + my $tagh = $self -> {"dbh"} -> prepare("SELECT m.*,t.name,t.creator_id,t.created + FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." AS m, + ".$self -> {"settings"} -> {"database"} -> {"tags"}." AS t + WHERE t.id = m.tag_id + AND m.metadata_id = ? + ORDER BY ".($alphasort ? "t.name ASC, m.rating DESC" : "m.rating DESC, t.name ASC")); + $tagh -> execute($metadataid) + or $self -> self_error("Unable to perform tag lookup query: ".$self -> {"dbh"} -> errstr); + + # Get the results as a reference to an array of hash refs. + return $tagh -> fetchall_arrayref({}); +} + + +## @method $ detach($metadataid, $tagid, $userid) +# Remove the tag from the specified metadata context. This will do nothing if the tag is +# not attached to the context, returning true if the tag is not set on the context (but +# potentially logging the attempt as a warning). No permission checks are (or can be) done +# by this method: the caller is required to ensure that the user performing the tag +# removal has permission to do so. +# +# @param metadataid The ID of the metadata context to remove the tag from. +# @param tagid The ID of the tag to remove. +# @param userid The ID of the user doing the removal. +# @return true on success (tag is no longer attached, or never was), under on error. +sub detach { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + + my $tagdata = $self -> _fetch_attached_tag($metadataid, $tagid); + return undef if($self -> {"errstr"}); # Was an error encountered in the fetch? + + # No need to do anything if the tag is not attached, but log it as an error anyway + if(!$tagdata) { + $self -> {"logger"} -> log("warning", $userid, undef, "Attempt to remove unattached tag $tagid from metadata $metadataid by $userid."); + return 1; + } + + # Tag is set, so remove it + my $nukeh = $self -> {"dbh"} -> prepare("DELETE FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + WHERE metadata_id = ? + AND tag_id = ?"); + my $rows = $nukeh -> execute($metadataid, $tagid); + return $self -> self_error("Unable to perform metadata tag removal: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Metadata tag removal failed: no rows modified") if($rows eq "0E0"); + + # Tag is gone, log the removal + $self -> _log_action($metadataid, $tagid, "delete", $userid, $tagdata -> {"rating"}); + + # Detach from the context + return defined($self -> {"metadata"} -> detach($metadataid)); +} + + +## @method $ rate_up($metadataid, $tagid, $userid) +# Rate up the specified tag in the metadata context, marking the provided user as the +# person doing the rating. Note that this will not do any permission checking - the +# caller must have established that the user has permission to update the rating. +# +# @param metadataid The ID of the metadata context containing the tag to rate. +# @param tagid The ID of the tag to change the rating of. +# @param userid The ID of the user performing the rating change. +# @return true on success, undef on error. +sub rate_up { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + + return $self -> _update_rating($metadataid, $tagid, $userid, 1); +} + + +## @method $ rate_down($metadataid, $tagid, $userid) +# Rate down the specified tag in the metadata context, marking the provided user as the +# person doing the rating. Note that this will not do any permission checking - the +# caller must have established that the user has permission to update the rating. +# +# @param metadataid The ID of the metadata context containing the tag to rate. +# @param tagid The ID of the tag to change the rating of. +# @param userid The ID of the user performing the rating change. +# @return true on success, undef on error. +sub rate_down { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + + return $self -> _update_rating($metadataid, $tagid, $userid, 0); +} + + +## @method $ user_has_rated($metadataid, $tagid, $userid) +# Determine whether the user has rated the specified tag in the metadata context. Note that +# this counts adding a tag as rating it - ie: users who added a tag automatically rate it as +# the default rating. This will only check as far back as the addition of a tag, if a tag has +# been added to a context, rated by a user, and then deleted and re-added, the user is not +# counted as having rated it (that is, this only checks the latest attachement of a tag). +# +# @param metadataid The ID of the metadata context to check for tag ratings. +# @param tagid The ID of the tag to look for ratings of. +# @param userid The ID of the user to look for when checking ratings. +# @return true if the user has rated the tag, false otherwise, and undef on error. +sub user_has_rated { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + + # Fetch the history of the tag, sorted in descending chronological order + # (ie: latest change first) filtered on the user's id or addition events. + # This will mean that the first row fetched is either a rating change by + # the user, or an addition event (possibly also by the user), so only one + # row is needed to determine whether the user has rated the tag. + my $histh = $self -> {"dbh"} -> prepare("SELECT user_id FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags_log"}." + WHERE metadata_id = ? + AND tag_id = ? + AND (user_id = ? OR event = 'added') + ORDER BY event_time DESC + LIMIT 1"); + $histh -> execute($metadataid, $tagid, $userid) + or return $self -> self_error("Unable to perform rating check query: ".$self -> {"dbh"} -> errstr); + + # If there's no row here, something has gone Badly Wrong (probably the tag isn't attached) + my $histrow = $histh -> fetchrow_arrayref(); + return $self -> self_error("Unexpected empty result from rating check query: no history for $tagid in $metadataid?") + if(!$histrow); + + # If the user_id in the fetched row matches $userid, the user has rated the + # tag implicitly (by adding it) or explicitly (by up/down rating it) + return $histrow -> [0] == $userid; +} + + +# ============================================================================ +# Private functions + +## @method private $ _attached($metadataid, $tagid) +# Determine whether the specified tag is set in the metadata context. This will check +# whether the tag has been attached to the specified context, and return true if it is. +# +# @param metadataid The ID of the metadata context to check for the tag. +# @param tagid The ID of the tag to look for. +# @return true if the tag is attached to the context, false if it is not, and undef on error. +sub _attached { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + + $self -> clear_error(); + + my $tagh = $self -> {"dbh"} -> prepare("SELECT id FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + WHERE metadata_id = ? + AND tag_id = ?"); + $tagh -> execute($metadataid, $tagid) + or return $self -> self_error("Unable to execute metadata tag lookup: ".$self -> {"dbh"} -> errstr); + + my $tag = $tagh -> fetchrow_arrayref(); + return defined($tag); +} + + +## @method private $ _fetch_attached_tag($metadataid, $tagid) +# Obtain the attachment data for the specified tag on a metadata context. This attempts to +# fetch the data associated with an attached tag - who attached it, when, what its current +# rating is - and returns a reference to a hash containing that data. +# +# @param metadataid The ID of the metadata context to check for the tag. +# @param tagid The ID of the tag to look for. +# @return A reference to a hash containing the attached tag's data, or undef if the tag is +# not attached, or on error. +sub _fetch_attached_tag { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + + $self -> clear_error(); + + my $tagh = $self -> {"dbh"} -> prepare("SELECT * FROM ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + WHERE metadata_id = ? + AND tag_id = ?"); + $tagh -> execute($metadataid, $tagid) + or return $self -> self_error("Unable to execute metadata tag lookup: ".$self -> {"dbh"} -> errstr); + + return $tagh -> fetchrow_hashref(); +} + + +## @method private $ _fetch_tagid($name) +# Given a tag name, attempt to find a tag record for that name. This will locate the +# first defined tag whose name matches the provided name. Note that if there are +# duplicate tags in the system, this will never find duplicates - it is guaranteed to +# find the tag with the lowest ID whose name matches the provided value, or nothing. +# +# @param name The name of the tag to find. +# @return The ID of the tag with the specified name on success, undef if the tag +# does not exist or an error occurred. +sub _fetch_tagid { + my $self = shift; + my $name = shift; + + $self -> clear_error(); + + # Does the tag already exist + my $tagid = $self -> {"dbh"} -> prepare("SELECT id FROM ".$self -> {"settings"} -> {"database"} -> {"tags"}." + WHERE name LIKE ?"); + $tagid -> execute($name) + or return $self -> self_error("Unable to perform tag lookup: ".$self -> {"dbh"} -> errstr); + my $tagrow = $tagid -> fetchrow_arrayref(); + + # Return the ID if found, undef otherwise + return $tagrow ? $tagrow -> [0] : undef;; +} + + +## @method private $ _update_rating($metadataid, $tagid, $userid, $increment) +# Update the rating for the tag in the specified metadata context. This increments or +# decrements the rating for the tag, marking the specified user as the user doing the +# rating change. Note that this does not (and can not) perform any permission checking: +# the caller must ensure that the user has permission to rate the tag. +# +# @param metadataid The ID of the metadata context containing the tag to rate. +# @param tagid The ID of the tag to change the rating of. +# @param userid The ID of the user performing the rating change. +# @param increment If true, the rating is incremented, otherwise it is decremented. +# @return true on success, undef otherwise. +sub _update_rating { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $userid = shift; + my $increment = shift; + + $self -> clear_error(); + + # Get the tag, which will include the current rating and confirm the tag is attached... + my $tag = $self -> _fetch_attached_tag($metadataid, $tagid); + return undef if($self -> {"errstr"}); # Was an error encountered in the fetch? + return $self -> self_error("Unable to update rating for $tagid in $metadataid: tag is not attached|") if(!$tag); + + # Got a tag, update the rating + $tag -> {"rating"} += ($increment ? 1 : -1); + + my $rateh = $self -> {"dbh"} -> prepare("UPDATE ".$self -> {"settings"} -> {"database"} -> {"metadata_tags"}." + SET rating = ? + WHERE id = ?"); + my $rows = $rateh -> execute($tag -> {"rating"}, $tag -> {"id"}); + return $self -> self_error("Unable to perform rating update: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Rating update failed: no rows modified") if($rows eq "0E0"); + + # Rating has been updated, log the update + return $self -> _log_action($metadataid, $tagid, "rate ".($increment ? "up" : "down"), $userid, $tag -> {"rating"}); +} + + +## @method private $ _log_action($metadataid, $tagid, $event, $userid, $rating) +# Log an action on an attached (or newly detached) tag. This allows the history of a tag +# to be tracked over its lifetime of attachment to a resource. +# +# @param metadataid The ID of the metadata the event happened in. +# @param tagid The ID of the tag involved in the event. +# @param event The event to be logged, must be 'added', 'deleted', 'rate up', +# 'rate down', 'activate', or 'deactivate'. +# @param userid The ID of the user who caused the event. +# @param rating The rating set on the tag after the operation. +# @return true on success, undef on error. +sub _log_action { + my $self = shift; + my $metadataid = shift; + my $tagid = shift; + my $event = shift; + my $userid = shift; + my $rating = shift; + + $self -> clear_error(); + + my $acth = $self -> {"dbh"} -> prepare("INSERT INTO ".$self -> {"settings"} -> {"database"} -> {"metadata_tags_log"}." + (metadata_id, tag_id, event, event_user, event_time, rating) + VALUES(?, ?, ?, ?, UNIX_TIMESTAMP(), ?)"); + my $rows = $acth -> execute($metadataid, $tagid, $event, $userid, $rating); + return $self -> self_error("Unable to perform metadata tag log insert: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Metadata tag log insert failed: no rows modified") if($rows eq "0E0"); + + return 1; +} + +1; diff --git a/supportfiles/.htaccess b/supportfiles/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/supportfiles/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/supportfiles/Doxyfile b/supportfiles/Doxyfile new file mode 100644 index 0000000..237ac69 --- /dev/null +++ b/supportfiles/Doxyfile @@ -0,0 +1,2282 @@ +# Doxyfile 1.8.5 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Aviary" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is included in +# the documentation. The maximum height of the logo should not exceed 55 pixels +# and the maximum width should not exceed 200 pixels. Doxygen will copy the logo +# to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = autodocs + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese- +# Traditional, Croatian, Czech, Danish, Dutch, English, Esperanto, Farsi, +# Finnish, French, German, Greek, Hungarian, Italian, Japanese, Japanese-en, +# Korean, Korean-en, Latvian, Norwegian, Macedonian, Persian, Polish, +# Portuguese, Romanian, Russian, Serbian, Slovak, Slovene, Spanish, Swedish, +# Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = YES + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = . + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a +# new page for each member. If set to NO, the documentation of a member will be +# part of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = "copy=\par Copyright:\n© " + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL. For instance to make +# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C +# (default is Fortran), use: inc=Fortran f=C. +# +# Note For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO these classes will be included in the various overviews. This option has +# no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. +# The default value is: NO. + +SORT_BRIEF_DOCS = YES + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = YES + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the +# todo list. This list is created by putting \todo commands in the +# documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the +# test list. This list is created by putting \test commands in the +# documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES the list +# will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = supportfiles/DoxygenLayout.xml + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. Do not use file names with spaces, bibtex cannot handle them. See +# also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO doxygen will only warn about wrong or incomplete parameter +# documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. + +INPUT = "." + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. + +FILE_PATTERNS = *.pm \ + *.pl \ + *.md + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = mdfiles + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = doxygenfilter + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER ) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = NO + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES, then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = newsagent + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = supportfiles/customdoxygen.css + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user- +# defined cascading style sheet that is included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefor more robust against future updates. +# Doxygen will copy the style sheet file to the output directory. For an example +# see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the stylesheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = YES + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler ( hhc.exe). If non-empty +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated ( +# YES) or that it should be included in the master .chm file ( NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated ( +# YES) or a normal table of contents ( NO) in the .chm file. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using prerendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://www.mathjax.org/mathjax + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /

***info***
diff --git a/templates/default/api/html_wrapper.tem b/templates/default/api/html_wrapper.tem new file mode 100755 index 0000000..2b022e8 --- /dev/null +++ b/templates/default/api/html_wrapper.tem @@ -0,0 +1,7 @@ + + + +API Response + +***data*** + diff --git a/templates/default/css/body.css b/templates/default/css/body.css new file mode 100755 index 0000000..8dbe8af --- /dev/null +++ b/templates/default/css/body.css @@ -0,0 +1,43 @@ +body { + font: 100% Verdana, Arial, Helvetica, sans-serif; + color: #000; + font-size: 13px; + min-width: 900px; + background-color: #eee; + height: 100%; + margin: 0; + padding: 0; +} + +body, h1, h2, h3, h4, h5 { + margin: 0; + padding: 0; +} + +a { + text-decoration: none; + color: #0645ad; + background: none; +} +a:visited { + color: #0b0080; +} +a:active { + color: #faa700; +} +a:hover, a:focus { + text-decoration: underline; +} + +.alignleft { + text-align: left; +} + +.alignright { + text-align: right; +} + +p { + margin: 0px; + padding: 0px; +} \ No newline at end of file diff --git a/templates/default/css/button.css b/templates/default/css/button.css new file mode 100755 index 0000000..216cc58 --- /dev/null +++ b/templates/default/css/button.css @@ -0,0 +1,167 @@ +/* based on http://webdesignerwall.com/tutorials/css3-gradient-buttons */ + +.button { + display: inline-block; + zoom: 1; /* zoom and *display = ie7 hack for display:inline-block */ + *display: inline; + vertical-align: baseline; + margin: 0 2px; + outline: none; + cursor: pointer; + text-align: center; + text-decoration: none; + font: 14px/100% Arial, Helvetica, sans-serif; + font-weight: bold; + padding: .5em 2em .55em; + text-shadow: 0 1px 1px rgba(0,0,0,.3); + -webkit-border-radius: .5em; + -moz-border-radius: .5em; + border-radius: .5em; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.2); + box-shadow: 0 1px 2px rgba(0,0,0,.2); +} +.button a { + text-align: center; + text-decoration: none; + font: 14px/100% Arial, Helvetica, sans-serif; + font-weight: bold; +} +.button a:hover { + text-decoration: none; +} +.button a:active { + text-decoration: none; +} + +.button:hover { + text-decoration: none; +} +.button:active { + position: relative; + top: 1px; +} + +/* blue */ +.button.blue { + color: #d9eef7; + border: solid 1px #0076a3; + + background-color: #0095cd; /* Fallback */ + background-image: -ms-linear-gradient(top, #00adee, #0078a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #00adee, #0078a5); /* Firefox */ + background-image: -o-linear-gradient(top, #00adee, #0078a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#00adee), to(#0078a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #00adee, #0078a5); /* new Webkit */ + background-image: linear-gradient(top, #00adee, #0078a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00adee', endColorstr='#0078a5'); +} +.button.blue:hover { + background-color: #007ead; /* Fallback */ + background-image: -ms-linear-gradient(top, #0095cc, #00678e); /* IE10 */ + background-image: -moz-linear-gradient(top, #0095cc, #00678e); /* Firefox */ + background-image: -o-linear-gradient(top, #0095cc, #00678e); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#0095cc), to(#00678e)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #0095cc, #00678e); /* new Webkit */ + background-image: linear-gradient(top, #0095cc, #00678e); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0095cc', endColorstr='#00678e'); +} +.button.blue:active { + background-color: #80bed6; /* Fallback */ + background-image: -ms-linear-gradient(top, #0078a5, #00adee); /* IE10 */ + background-image: -moz-linear-gradient(top, #0078a5, #00adee); /* Firefox */ + background-image: -o-linear-gradient(top, #0078a5, #00adee); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#0078a5), to(#00adee)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #0078a5, #00adee); /* new Webkit */ + background-image: linear-gradient(top, #0078a5, #00adee); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0078a5', endColorstr='#00adee'); +} +.button.blue a { + color: #d9eef7; +} + +/* red */ +.button.red { + color: #d9eef7; + border: solid 1px #a30000; + + background-color: #cc0015; /* Fallback */ + background-image: -ms-linear-gradient(top, #ed0034, #A60000); /* IE10 */ + background-image: -moz-linear-gradient(top, #ed0034, #A60000); /* Firefox */ + background-image: -o-linear-gradient(top, #ed0034, #A60000); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#ed0034), to(#A60000)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #ed0034, #A60000); /* new Webkit */ + background-image: linear-gradient(top, #ed0034, #A60000); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ed0034', endColorstr='#A60000'); +} +.button.red:hover { + background-color: #ad0000; /* Fallback */ + background-image: -ms-linear-gradient(top, #cc0000, #8f0000); /* IE10 */ + background-image: -moz-linear-gradient(top, #cc0000, #8f0000); /* Firefox */ + background-image: -o-linear-gradient(top, #cc0000, #8f0000); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#cc0000), to(#8f0000)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #cc0000, #8f0000); /* new Webkit */ + background-image: linear-gradient(top, #cc0000, #8f0000); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#cc0000', endColorstr='#8f0000'); +} +.button.red:active { + background-color: #80bed6; /* Fallback */ + background-image: -ms-linear-gradient(top, #A60000, #ed0034); /* IE10 */ + background-image: -moz-linear-gradient(top, #A60000, #ed0034); /* Firefox */ + background-image: -o-linear-gradient(top, #A60000, #ed0034); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#A60000), to(#ed0034)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #A60000, #ed0034); /* new Webkit */ + background-image: linear-gradient(top, #A60000, #ed0034); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#A60000', endColorstr='#ed0034'); +} +.button.red a { + color: #d9eef7; +} + + +/* disabled */ +.button.disabled { + color: #555; + border: solid 1px #a3a3a3; + + background-color: #cdcdcd; /* Fallback */ + background-image: -ms-linear-gradient(top, #eeeeee, #a5a5a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #eeeeee, #a5a5a5); /* Firefox */ + background-image: -o-linear-gradient(top, #eeeeee, #a5a5a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#a5a5a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #eeeeee, #a5a5a5); /* new Webkit */ + background-image: linear-gradient(top, #eeeeee, #a5a5a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#a5a5a5'); +} + +.button.disabled:hover { + color: #555; + border: solid 1px #a3a3a3; + + background-color: #cdcdcd; /* Fallback */ + background-image: -ms-linear-gradient(top, #eeeeee, #a5a5a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #eeeeee, #a5a5a5); /* Firefox */ + background-image: -o-linear-gradient(top, #eeeeee, #a5a5a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#a5a5a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #eeeeee, #a5a5a5); /* new Webkit */ + background-image: linear-gradient(top, #eeeeee, #a5a5a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#a5a5a5'); +} + +.button.disabled:active { + color: #555; + border: solid 1px #a3a3a3; + + background-color: #cdcdcd; /* Fallback */ + background-image: -ms-linear-gradient(top, #eeeeee, #a5a5a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #eeeeee, #a5a5a5); /* Firefox */ + background-image: -o-linear-gradient(top, #eeeeee, #a5a5a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#a5a5a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #eeeeee, #a5a5a5); /* new Webkit */ + background-image: linear-gradient(top, #eeeeee, #a5a5a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#a5a5a5'); +} + +.button.disabled a { + color: #555; +} diff --git a/templates/default/css/controls.css b/templates/default/css/controls.css new file mode 100755 index 0000000..3f3feb4 --- /dev/null +++ b/templates/default/css/controls.css @@ -0,0 +1,31 @@ +div.controls { + text-align: right; +} + +ul.controls { + padding: 0px; + margin: 0px; +} + +ul.controls > li { + list-style-type: none; + padding-left: 5px; + display: inline; +} + +.control { + cursor: pointer; + width: 16px; + height: 16px; + background-image: url('../images/control_sprites.png'); + background-repeat: no-repeat; + background-position: 0px 0px; + display: inline-block; +} + +.control > img { + padding: 0px; + margin: 0px; + border: none; + opacity: 0; +} diff --git a/templates/default/css/core.css b/templates/default/css/core.css new file mode 100755 index 0000000..188519c --- /dev/null +++ b/templates/default/css/core.css @@ -0,0 +1,333 @@ +@charset "utf-8"; +@import url('body.css'); /* global body styling */ +@import url('notebox.css'); /* pull in warning/important box styles */ +@import url('shadowbox.css'); /* and support for shadowed divs */ +@import url('messagebox.css'); /* support for message boxes */ +@import url('button.css'); /* button rules (okay?) */ +@import url('inputglow.css'); /* make input boxes glow when active */ +@import url('controls.css'); /* sprite-based control buttons */ + +.aviary #topbar { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 36px; + background-image: url(../images/topbar_bg.png); + background-repeat: repeat-x; +} + +.aviary #titleblock { + line-height: 36px; + float: left; + margin-left: 15px; + font-weight: bold; + padding-right: 10px; + background-image: url(../images/topbar_div.png); + background-repeat: no-repeat; + background-position: right center; +} + +.aviary #topbar ul { + float: right; + padding: 0px; + margin: 0px; +} + +.aviary #topbar ul li { + line-height: 36px; + float: left; + list-style: none; + padding-left: 10px; + padding-right: 10px; + background-image: url(../images/topbar_div.png); + background-repeat: no-repeat; + background-position: left center; +} + +.aviary .footer { + text-align: center; + width: 50%; + margin: 0px auto; + font-size: 11px; + color: #777; +} + +.aviary #botbar { + position: absolute; + left: 0px; + bottom: 0px; + width: 100%; + height: 46px; + background-image: url(../images/botbar_bg.png); + background-repeat: repeat-x; +} + +.aviary #botbar ul { + float: right; + padding: 0px; + margin: 0px; + padding-right: 10px; +} + +.aviary #botbar ul li { + line-height: 46px; + float: left; + list-style: none; + padding-left: 5px; + padding-right: 5px; +} + +.aviary #botbar #navblock { + position: absolute; + left: 0px; + bottom: 0px; + width: 100%; + height: 46px; + text-align: center; +} + +.aviary #botbar #navblock #navinner { + display: inline-block; +} + +.aviary #botbar #navblock #navinner #leftbar { + float: left; + width: 5px; + height: 46px; + background-image: url(../images/botbar_div.png); + background-repeat: no-repeat; + background-position: left center; +} + +.aviary #botbar #navblock #navinner #rightbar { + float: right; + width: 5px; + height: 46px; + background-image: url(../images/botbar_div.png); + background-repeat: no-repeat; + background-position: right center; +} + +.aviary #botbar #navblock #navinner #navfloat { + float: left; + margin-top: 6px; +} + +.aviary #botbar #navblock #navinner #navfloat #navbox { + margin: auto; + border-collapse: collapse; +} + +.aviary #botbar #navblock #navinner #navfloat #navbox td { + padding: 3px 3px; +} + +.aviary #botbar #navblock #navinner #navfloat #navbox td.navleft { + vertical-align: middle; + text-align: left; + padding-right: 4px; +} + +.aviary #botbar #navblock #navinner #navfloat #navbox td.navright { + vertical-align: middle; + text-align: right; + padding-left: 4px; +} + +.aviary #displaycore { + padding-top: 36px; + padding-bottom: 46px; + position: relative; + overflow: hidden; +} + +.aviary #displaycore div.imageframe { + position: absolute; + border: 1px solid red; +} + +.aviary #content { + margin-top: 40px; +} + +.navpoint { + border: none; + margin: 0px; + padding: 0px; + width: 5px; + height: 5px; + display: block; +} + +.navend { + display: block; + width: 1px; + height: 36px; +} + +label { + font-weight: bold; + font-size: 13px; +} + +label.check { + font-weight: normal; + font-size: 13px; + padding-left: 0.75em; +} + +.overTxtLabel { + color: #aaa; +} + +hr { + border: none; + border-bottom: 1px solid #DDDDDD; + clear: both; + height: 0px; + margin: 15px 0px; + overflow: hidden; +} + +input.fillwide, select.fillwide, textarea.fillwide { + width: 100%; +} + +.fillwide.lockwide { + max-width: 100%; +} + +input[type=text].datepick { + padding: 4px 4px 4px 23px; + background: url(../images/datepicker.png) no-repeat 2px center; +} + +input:disabled.datepick { + background-color: #eee; +} + +.submitbox { + text-align: right; +} + +ul.tabs { + border-bottom: 1px solid #aaa; + margin-bottom: 0px; + padding: 0px 0px 2px; + margin-top: 1em; +} + +li.tab { + cursor: pointer; + display: inline; + padding: 4px .4em 2px .4em; + list-style: none; + border: 1px #ddd solid; + border-bottom: 1px #aaa solid; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background-color: #eee; +} + +li.tab.active { + background-color: #fff; + border: 1px #aaa solid; +} + +div.paginate { + text-align: right; + margin: 4px auto; +} + +div.paginate div.location { + display: inline-block; +} + +ul.paginate { + display: inline-block; + margin: 0px; + padding: 0px; +} + +ul.paginate li { + display: inline; + list-style: none; +} + +ul.paginate li.pagebox { + color: #d9eef7; + border: solid 1px #0076a3; + margin: 0 0; + outline: none; + cursor: pointer; + text-align: center; + text-decoration: none; + font: 14px/100% Arial, Helvetica, sans-serif; + font-weight: bold; + padding: .3em 1em .3em; + text-shadow: 0 1px 1px rgba(0,0,0,.3); + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.2); + box-shadow: 0 1px 2px rgba(0,0,0,.2); + + background-color: #0095cd; /* Fallback */ + background-image: -ms-linear-gradient(top, #00adee, #0078a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #00adee, #0078a5); /* Firefox */ + background-image: -o-linear-gradient(top, #00adee, #0078a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#00adee), to(#0078a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #00adee, #0078a5); /* new Webkit */ + background-image: linear-gradient(top, #00adee, #0078a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00adee', endColorstr='#0078a5'); +} + +ul.paginate li.pagebox:hover { + background-color: #007ead; /* Fallback */ + background-image: -ms-linear-gradient(top, #0095cc, #00678e); /* IE10 */ + background-image: -moz-linear-gradient(top, #0095cc, #00678e); /* Firefox */ + background-image: -o-linear-gradient(top, #0095cc, #00678e); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#0095cc), to(#00678e)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #0095cc, #00678e); /* new Webkit */ + background-image: linear-gradient(top, #0095cc, #00678e); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0095cc', endColorstr='#00678e'); +} + +ul.paginate li.pagebox.disabled, ul.paginate li.pagebox.disabled:hover { + color: #888; + border: solid 1px #a3a3a3; + cursor: auto; + + background-color: #cdcdcd; /* Fallback */ + background-image: -ms-linear-gradient(top, #eeeeee, #a5a5a5); /* IE10 */ + background-image: -moz-linear-gradient(top, #eeeeee, #a5a5a5); /* Firefox */ + background-image: -o-linear-gradient(top, #eeeeee, #a5a5a5); /* Opera */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#a5a5a5)); /* old Webkit */ + background-image: -webkit-linear-gradient(top, #eeeeee, #a5a5a5); /* new Webkit */ + background-image: linear-gradient(top, #eeeeee, #a5a5a5); /* proposed W3C Markup */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#a5a5a5'); +} + +ul.paginate li.pagebox a { + color: #d9eef7; + text-decoration: none; +} + + +ul.paginate li.pagebox:first-child { + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} + +ul.paginate li.pagebox:last-child { + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; +} + +li.pagebox.active { +} + +code, tt { + font-face: fixed; + color: #a96300; + background: inherit !important; +} diff --git a/templates/default/css/error.css b/templates/default/css/error.css new file mode 100755 index 0000000..98ac0ed --- /dev/null +++ b/templates/default/css/error.css @@ -0,0 +1,29 @@ +@charset "utf-8"; +@import url('body.css'); /* global body styling */ +@import url('notebox.css'); /* pull in warning/important box styles */ +@import url('shadowbox.css'); /* and support for shadowed divs */ +@import url('messagebox.css'); /* support for message boxes */ +@import url('button.css'); /* button rules (okay?) */ +@import url('userbar.css'); /* Userbar styling. */ +@import url('inputglow.css'); /* make input boxes glow when active */ + +#errorcore { + margin: 0px auto; + padding: 100px 0px 0px; + width: 440px; +} + +#errorcore h1#logo a { + display: block; + width: 141px; + height: 78px; + background: url('../images/page_layout/logo.png') 50% top no-repeat; + padding-bottom: 10px; + margin: 0px auto; +} + +input[type=text], input[type=password] { + font-size: 20px; + margin-bottom: 10px; + margin-top: 2px; +} diff --git a/templates/default/css/hotimage.css b/templates/default/css/hotimage.css new file mode 100755 index 0000000..b7fa00e --- /dev/null +++ b/templates/default/css/hotimage.css @@ -0,0 +1,6 @@ + +img.hotimage { + cursor: pointer; + border: 0px; + margin: 0px 0px; +} \ No newline at end of file diff --git a/templates/default/css/inputglow.css b/templates/default/css/inputglow.css new file mode 100755 index 0000000..7f13be4 --- /dev/null +++ b/templates/default/css/inputglow.css @@ -0,0 +1,6 @@ + +input[type=text]:focus, input[type=password]:focus, input[type=checkbox]:focus, textarea:focus { + box-shadow: 0 0 5px rgba(0, 0, 255, 1); + -webkit-box-shadow: 0 0 5px rgba(0, 0, 255, 1); + -moz-box-shadow: 0 0 5px rgba(0, 0, 255, 1); +} diff --git a/templates/default/css/login.css b/templates/default/css/login.css new file mode 100755 index 0000000..1466d43 --- /dev/null +++ b/templates/default/css/login.css @@ -0,0 +1,91 @@ +@charset "utf-8"; +@import url('body.css'); /* global base body styles */ +@import url('notebox.css'); /* pull in warning/important box styles */ +@import url('shadowbox.css'); /* and support for shadowed divs */ +@import url('messagebox.css'); /* support for message boxes */ +@import url('button.css'); /* button rules (okay?) */ +@import url('inputglow.css'); /* make input boxes glow when active */ + +#logincore { + margin: 0px auto; + padding: 100px 0px 0px; +} + +#logincore h1#logo a { + display: block; + width: 150px; + height: 124px; + background: url('../images/logo.png') 50% top no-repeat; + padding-bottom: 10px; + margin: 0px auto; +} + +div.logincore { + margin-left: auto; + margin-right: auto; + margin-top: 10px; + width: 440px; +} + +div.persist { + float: left; + text-align: left; +} + +.loginform { + width: 400px; + padding: 0px 20px; +} +.regform { + width: 400px; + padding: 0px 20px; + border-left: 1px solid #333; + margin-bottom: 8px; +} +.sbcontent { clear: both; } + +div.entry { + text-align: left; + margin: 0px auto; +} + +div.entry label { + color: #222; +} + +div.entry label input { + width: 100%; +} + +div.submit { + text-align: right; +} + +div.submit .contextlink { + float: left; + text-align: left; + font-size: 10px; +} + +div.persist .contextlink { + text-align: left; + font-size: 10px; +} + +div.loginform { + display: inline-block; + /*float: left;*/ +} +div.regform { + display: inline-block; + /*float: left;*/ +} +div.regform.policy { + float: right; +} + +input[type=text], input[type=password] { + font-size: 20px; + margin-bottom: 10px; + margin-top: 2px; +} diff --git a/templates/default/css/messagebox.css b/templates/default/css/messagebox.css new file mode 100755 index 0000000..19361f4 --- /dev/null +++ b/templates/default/css/messagebox.css @@ -0,0 +1,46 @@ +.messagebox .mcore { + width: 100%; + clear: both; +} + +.messagebox > img { + padding: 0px; + border: none; + float: right; + margin-top: -2px; /* adjust up slightly to v-centre */ +} + +.messagebox .mcore .msummary { + padding: 0; + border: none; + text-align: center; + font-weight: bold; +} + +.messagebox .mcore .mtext { + padding: 0; + margin-top: 0.5em; + border: none; + text-align: center; +} + +.messagebox .buttonbox { + padding-top: 3px; + margin-top: 2px; + border-top: 1px solid #ddd; + text-align: right; +} + +.messagecore { + margin: 100px auto !important; + width: 440px; +} + +.messagecore h1#logo a { + display: block; + width: 141px; + height: 78px; + background: url('../images/page_layout/logo.png') 50% top no-repeat; + padding-bottom: 10px; + margin: 0px auto; +} diff --git a/templates/default/css/notebox.css b/templates/default/css/notebox.css new file mode 100755 index 0000000..8a00123 --- /dev/null +++ b/templates/default/css/notebox.css @@ -0,0 +1,43 @@ +table.notebox { + margin: 0px 10%; /* 10% = Will not overlap with other elements */ + border: 1px solid #aaa; + border-left: 10px solid #1e90ff; /* Default "notice" blue */ + background: #fbfbfb; + display: block; + text-align: left; + border-radius: 1px; + -moz-box-shadow: 3px 3px 4px rgba(0,0,0,0.1); + -webkit-box-shadow: 3px 3px 4px rgba(0,0,0,0.1); + box-shadow: 3px 3px 4px rgba(0,0,0,0.1); + margin-bottom: 4px; +} +table.ienotebox { + margin: 0px 10%; /* 10% = Will not overlap with other elements */ + border: 1px solid #aaa; + border-left: 10px solid #1e90ff; /* Default "notice" blue */ + background: #fbfbfb; +} + +table.notebox + table.notebox { /* Single border between stacked boxes. */ + margin-top: -5px; +} +th.nbox-text, +td.nbox-text { /* The message body cell(s) */ + padding: 0.25em 0.5em; /* 0.5em left/right */ +} +td.nbox-image { /* The left image cell */ + padding: 2px 0 2px 0.5em; /* 0.5em left, 0px right */ +} +td.nbox-imageright { /* The right image cell */ + padding: 2px 0.5em 2px 0; /* 0px left, 0.5em right */ +} + +table.notebox-notice { + border-left: 10px solid #1e90ff; /* Blue */ +} +table.notebox-warning { + border-left: 10px solid #e1b416; /* Orange */ +} +table.notebox-error { + border-left: 10px solid #b22222; /* Red */ +} diff --git a/templates/default/css/orb.css b/templates/default/css/orb.css new file mode 100755 index 0000000..007dc69 --- /dev/null +++ b/templates/default/css/orb.css @@ -0,0 +1,47 @@ +#aviary-core { + padding-top: 40px; + margin: 0px auto; + width: 1000px; +} + +#aviary-core > ul { + float: left; + margin: 0px; + padding: 0px; +} + +#aviary-core > ul > li { + list-style-type: none; + margin: 0px 0px 10px 0px; + overflow: hidden; +} + +dl.input { + margin: 5px 0px 0px 0px; +} + +dl.input > dt { + margin: 0px 0px 6px; +} + +dl.input > dd { + margin: 0px; + padding: 0px; + white-space: nowrap; +} + +dl.input > dd > select { + min-width: 10em; +} + +dl.input > dd > input[type="radio"] { + width: 16px; +} + +.submitbox { + margin-top: 0.5em; +} + +img.workspin { + opacity: 0; +} diff --git a/templates/default/css/shadowbox.css b/templates/default/css/shadowbox.css new file mode 100755 index 0000000..f1c2333 --- /dev/null +++ b/templates/default/css/shadowbox.css @@ -0,0 +1,40 @@ +.shadowbox { + border: 1px solid #ddd; + border-radius: 4px; + -moz-box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + -webkit-box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + margin: 0px 0px 10px; + overflow: hidden; + padding: 8px 10px 0px; + background-color: #fff; +} + +.shadowbox.core { + margin: 0px auto; + margin-top: 40px; + width: 1130px; +} + +.shadowbox > h2 { + font-size: 14px; + margin: 0px; + white-space: nowrap; + margin: 0; + padding: 0; + font-weight: bold; + color: #555; +} + +.shadowbox div.sbcontent { + border-top: 1px solid #eee; + color: #000; + background: #f4f4f4; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + margin-left: -10px; + margin-top: 8px; + padding: 5px 10px; + width: 100%; + clear: both; +} \ No newline at end of file diff --git a/templates/default/css/userbar.css b/templates/default/css/userbar.css new file mode 100755 index 0000000..8b57374 --- /dev/null +++ b/templates/default/css/userbar.css @@ -0,0 +1,249 @@ +/* CSS rules for the site-wide user bar that appears at the top + * of the page. This have been kept separate from site.css and + * other stylesheets to reduce madness therein. + * + * Bits of this are based on, or blatantly lifted from, the WordPress + * admin-bar.css under the terms of the GNU GPL v2. + */ +#userbar { + color: #ccc; + font: normal 13px/28px Arial,Verdana,sans-serif; + height: 28px; + position: fixed; + top: 0; + left: 0; + width: 100%; + min-width: 900px; /* must match min-width in site.css */ + z-index: 2317; /* /o\ */ + + background-color: #464646; /* Fallback */ + background-image: -ms-linear-gradient(bottom, #373737, #464646 5px); /* IE10 */ + background-image: -moz-linear-gradient(bottom, #373737, #464646 5px); /* Firefox */ + background-image: -o-linear-gradient(bottom, #373737, #464646 5px); /* Opera */ + background-image: -webkit-gradient(linear, left bottom, left top, from(#373737), to(#464646)); /* old Webkit */ + background-image: -webkit-linear-gradient(bottom, #373737, #464646 5px); /* new Webkit */ + background-image: linear-gradient(bottom, #373737, #464646 5px); /* proposed W3C Markup */ +} + +#userbar * { + margin: 0; + padding: 0; + position: static; + line-height: 1; + font: normal 13px/28px sans-serif; + color: #ccc; + text-shadow: #444 0px -1px 0px; + width: auto; +} + +/* overall container for the bar, has site-options on the left and user-options on the right */ +#userbar .bar-container { + padding-left: 1px; +} + +/* float the user-options menus to the right side */ +#userbar .user-options { + float: right; +} + +#userbar a, #userbar a:hover, #userbar a img, #userbar a img:hover { + outline: none; + border: none; + text-decoration: none; + background: none; + cursor: pointer; +} + +#userbar .menu-wrapper, #userbar ul, #userbar ul li { + background: none; + clear: none; + list-style: none; + margin: 0; + padding: 0; + position: relative; + z-index: 99999; +} + +#userbar .bar-container ul { + text-align: left; +} + +#userbar li { + float: left; +} + +#userbar .bar-container > ul > li { + border-right: 1px solid #555; +} + +#userbar .bar-container > ul > li > a, #userbar .bar-container > ul > li > .empty-item { + border-right: 1px solid #333; +} + +#userbar .bar-container .user-options > li { + border-left: 1px solid #333; + border-right: 0; + float: right; +} + +#userbar .bar-container .user-options > li > a, #userbar .bar-container .user-options > li > .empty-item { + border-left: 1px solid #555; + border-right: 0; +} + +#userbar .bar-container a, #userbar .bar-container .empty-item { + height: 28px; + display: block; + padding: 0 12px; + margin: 0; +} + +#userbar .menu .menu-wrapper { + margin: 0 0 0 -1px; + padding: 0; + -moz-box-shadow: 3px 3px 4px rgba(0,0,0,0.2); + -webkit-box-shadow: 3px 3px 4px rgba(0,0,0,0.2); + box-shadow: 3px 3px 4px rgba(0,0,0,0.2); + background: #fff; + display: none; + position: absolute; + float: none; + border-width: 0 1px 1px 1px; + border-style: solid; + border-color: #dfdfdf; + border-radius: 3px; +} + +#userbar .user-options .menu .menu-wrapper { + right: 0; + left: auto; + margin: 0 -1px 0 0; +} + +#userbar .menu-wrapper > .menu-submenu:first-child { + border-top: none; +} + +#userbar .menu-submenu { + padding: 6px 0; + border-top: 1px solid #dfdfdf; +} + +#userbar .bar-container .menu ul li { + float: none; +} + +#userbar .top-menu > li.menu:hover > .item, #userbar .top-menu > li.menu.hover > .item { + background: #fff; + color: #333; + text-shadow: none; +} + +#userbar .top-menu > li:hover > .item, #userbar .top-menu > li.hover > .item, #userbar .top-menu > li > .item:focus { + color: #fafafa; + background-color: #3a3a3a; /* Fallback */ + background-image: -ms-linear-gradient(bottom, #3a3a3a, #222); /* IE10 */ + background-image: -moz-linear-gradient(bottom, #3a3a3a, #222); /* Firefox */ + background-image: -o-linear-gradient(bottom, #3a3a3a, #222); /* Opera */ + background-image: -webkit-gradient(linear, left bottom, left top, from(#3a3a3a), to(#222)); /* old Webkit */ + background-image: -webkit-linear-gradient(bottom, #3a3a3a, #222); /* new Webkit */ + background-image: linear-gradient(bottom, #3a3a3a, #222); /* proposed W3C Markup */ +} + +#userbar .top-menu > li.menu:hover > .item, #userbar .top-menu > li.menu.hover > .item { + background: #fff; + color: #333; + text-shadow: none; +} + +#userbar .menu li:hover, +#userbar .menu li.hover, +#userbar .bar-container .menu .item:focus, +#userbar .bar-container .top-menu .menu .item:focus { + background-color: #eaf2fa; +} + +#userbar .menu-submenu .item { + color: #333; + text-shadow: none; +} + +#userbar li:hover > .menu-wrapper, +#userbar li.hover > .menu-wrapper { + display: block; +} + +#userbar .bar-container .menu ul li .item, +#userbar .bar-container .menu ul li a strong, +#userbar .bar-container .menu.hover ul li .item, +#userbar .bar-container .menu:hover ul li .item { + line-height: 26px; + height: 26px; + text-shadow: none; + white-space: nowrap; + min-width: 140px; +} + +#userbar .bar-container ul li a img.controlicon { + width: 16px; + height: 16px; + padding: 0; + line-height: 24px; + vertical-align: middle; +} + +/* User profile */ +#userbar #user-profile > a img { + width: 16px; + height: 16px; + border: 1px solid #999; + padding: 0; + background: #eee; + line-height: 24px; + vertical-align: middle; + float: none; + display: inline; +} + +#userbar #profile-actions li { + margin-left: 88px; +} + +#userbar #user-profile .menu-wrapper .menu-submenu .avatar { + position: absolute; + left: -72px; + top: 4px; +} + +#userbar #profile-display { + margin-top: 6px; + margin-bottom: 15px; + height: auto; + background: none; +} + +#userbar #profile-display a { + background: none; + height: auto; +} + +#userbar #profile-display span { + background: none; + padding: 0; + height: 18px; +} + +#userbar #profile-display .display-name, #userbar #profile-display .username { + text-shadow: none; + display: block; +} + +#userbar #profile-display .display-name { + color: #333; +} + +#userbar #profile-display .username { + color: #999; + font-size: 11px; + padding-left: 1.5em; +} diff --git a/templates/default/error/error_box.tem b/templates/default/error/error_box.tem new file mode 100755 index 0000000..f57cf0d --- /dev/null +++ b/templates/default/error/error_box.tem @@ -0,0 +1,6 @@ + + + + + +
error***message***
diff --git a/templates/default/error/error_item.tem b/templates/default/error/error_item.tem new file mode 100755 index 0000000..f6d6297 --- /dev/null +++ b/templates/default/error/error_item.tem @@ -0,0 +1 @@ +
  • ***error***
  • diff --git a/templates/default/error/error_list.tem b/templates/default/error/error_list.tem new file mode 100755 index 0000000..3fe34fd --- /dev/null +++ b/templates/default/error/error_list.tem @@ -0,0 +1,2 @@ +
    ***message***
    +
      ***errors***
    diff --git a/templates/default/error/general.tem b/templates/default/error/general.tem new file mode 100755 index 0000000..11387e2 --- /dev/null +++ b/templates/default/error/general.tem @@ -0,0 +1,33 @@ + + + + + ***title*** + + + + + + + + + + + + + + + + + + + ***extrahead*** + + +
    +

    + ***message*** +
    + ***userbar*** + + diff --git a/templates/default/images/calendar/addtweet.png b/templates/default/images/calendar/addtweet.png new file mode 100755 index 0000000000000000000000000000000000000000..3fa86e6138e805f5cc55cd7a254c032e836693c8 GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}EByV>Y zhW{YAVDIwDKoKQR7sn8d^T`PdgnsZJm1yV;nVhk{QSjthmzC*KoeN!z7#KtySa~|L S?`#07X7F_Nb6Mw<&;$Ttq$6|y literal 0 HcmV?d00001 diff --git a/templates/default/images/calendar/next.png b/templates/default/images/calendar/next.png new file mode 100755 index 0000000000000000000000000000000000000000..ac9b5881bf6ad1ad6948f80547e7dab8363b7fc9 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}EByV>Y zhW{YAVDIwDK#^ci7sn8d^J_0YNS%G}EByV>Y zhW{YAVDIwDK#^!q7sn8d^T`Pdgc4E{5@xuqNm68EYqRCy;rZVgxaQn}0|nfxW-MR} z2MT`o=-QZQaUv}t;fJ%s$9gs(Q1fy+Hnp+wA@fE*r*B_!lWcos)>M05bSOIVkd>iu Ww@R;?h0hD1H4L7velF{r5}E*aX*?|e literal 0 HcmV?d00001 diff --git a/templates/default/images/control_sprites.png b/templates/default/images/control_sprites.png new file mode 100755 index 0000000000000000000000000000000000000000..d67d4716c6712c2512865fb08cf9a4726bd55de8 GIT binary patch literal 6074 zcmai2g;!MF*S-S-GYm094;`X}#2}$`_sb9p0!m9ucMl;Vh)RnnjdTpDQbVJNG$Rhe zC=yZJbgaCVQ+E&}8wjvyVYAT3{SZx^2+Kr6t-G05csf2c>0JHMWRvFY7726g~oXEo5) zxD)nm`+nq|EPU(bn?G`Y9I2_3hIgm5&GbweHMmL_%VfQ6@kl;tZ)c{le&wxg<9y45 z%7K?JUoKw5KB+JBtbc1Hmz9Sz2CD3roI*{xa^oZYe6VFp#8hdg#4ncZM52Rkb3?0ayBt+9}^(AKX9MhpUBOYW8QpX?9iK zEd-R?fA)oRyfHUyIIS%!OZ=P5HNA8ksssQIUnd;Lx2-cff9ks|lh&5Xn=>*qyN-{K z_qE_EWp+I=U1~BinGj6>V|c1}VQ}|EritqNMDte=AThDq#QSCzBEGM$uQMYfBNs$; z_ut#JsSA-GZG?^bCkkXClBlStios&7uufdURl0JZvKF^_;L|w&uzjGr;p|lA79b)a z6@9ShW;poV9^cg(zOtp&5HoVdi#wH;mZk-Dx}lIwf>*9cJcjdKcsV)!^aaEz`9nV_ zga@vEj+^y-L~i{*FyuD6LLmUoGMAN??_1u(tsN9MHlpCTQGox+RFL)CDml4XD46nv zHcclk&e00RvU}8-obr@nJaFQ78d6z=HTdqvPL~BKdS&SkB>>3t^%K4=RY`2-MjiK^ zMNX}yaka~@{{;u&Ypu0rJ%jfLH#XMuWZZP|v-p+n{f(4wxpqhY^nj@C+cW}Ut9fn{ z;olyfa+SB7ySa&^a95d9HHjxUhr7>kR?c`<-Vs|5Rt!_N^W&zKMq-4h1yYzgc^jwz z7!3BZ`|A?y;yi}NU5j+3p{M2F-bguPD{b+Y3wreFT5r)!0mQCmx(hNW;$8l0XyYVO zP4tlW@HpB&xW&HF0TjL43z3fa>pp|W_iu-kHZ?KH$;s^=N4dMZt4Bwxz-Va+`04m~ ziffXKtC>OuI?y6hjv}yYGlxwY+t`?@$elQJ1Eqz?nJOPLhq514(9%+zRClj^XuxW@ zI>PJH_RPr8kQk)cu-xN?m(0q^VMva`Y#v>^dR0s(`{cH)Aar@B@Je_K``v6EVmLk0 zQkx%v(eMI>QOO2qba3HPXurM(*H!N;<;%$)r=823qfNDg59Rm9?D$%!Rb)No2GkL3 zMl5uUy4-3p+%Dx6>Np(25rd;8TCrW3o}N90<�)lG>Wii@-9UV!X?!R)9{99~ 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 UYVvj?|3wE3bkN%Mnhx>*2ZF|A6aWAK literal 0 HcmV?d00001 diff --git a/templates/default/images/datepicker.png b/templates/default/images/datepicker.png new file mode 100755 index 0000000000000000000000000000000000000000..0524cb37fe791f7cedffb0547f006f416eb0ec11 GIT binary patch literal 3299 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 Xq0QFqn(9Yl00000NkvXXu0mjfZ8t>xX|O+WQR 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 U+#`I}b^rhX07*qoM6N<$f>uAi761SM literal 0 HcmV?d00001 diff --git a/templates/default/images/info.png b/templates/default/images/info.png new file mode 100755 index 0000000000000000000000000000000000000000..079f8fc59e72f60a9fd0685a4e92421f333b2acd GIT binary patch literal 3829 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 TKB3Oow*t}OaXW5?@{0chln%~g literal 0 HcmV?d00001 diff --git a/templates/default/images/logo.png b/templates/default/images/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..78622924d5bb0c36cecd483a5d73e8046a0c6192 GIT binary patch literal 18286 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 MDP_qr3FCnO0lv8cUH||9 literal 0 HcmV?d00001 diff --git a/templates/default/images/messages/articleok22.png b/templates/default/images/messages/articleok22.png new file mode 100755 index 0000000000000000000000000000000000000000..9b96352661f40426b0f0760f9152dbf6f0790caf GIT binary patch literal 4121 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 X=mRtcCp}K~00000NkvXXu0mjfy%Nio literal 0 HcmV?d00001 diff --git a/templates/default/images/messages/error22.png b/templates/default/images/messages/error22.png new file mode 100755 index 0000000000000000000000000000000000000000..0b064c3e72a71b604f575709616fd413365f0c91 GIT binary patch literal 995 zcmV<9104K`P)O$Df0JFPre0M=1wtV&?fPz4we=Wd?s`7yqC0bX|CPnFBeSR4rK6gjs*F1Z z+Kx4Jn$}pS)wE;ZrjPy?<4?Pt(Hp0qSTJYP#(djpvgh{Y$CEPfpNz*_B-A;Qel<~_ zO%$>L6ifK2f;aq3+sDVFVy)M^E;y3%4{RUZ{BUV*hFd>PlKcKLT$-f!xh;eBwd&D- z1s-pgSbDVkNX)=J-?Tp}IYlnF|yb zH1&_aMttmLbkT?NQw$u4-j{6i4sQs2YO9q>k8F7(){s^6mzMFg1~g(cB~wOUGq^IN zcI@}YKiDjO)!=w*?Xz8derI9oGGZ$#w{E{qA(zuEO#)}G)87*`CR^Q?ssc}>Rcm_h zrq}(*BFoo{M0_FY{9Ri5P9r4<@&?z-(>rLtHNH)y?h3Bo96Qpr(@*6tTp^P3aAFpr z@~SPONHhy74EVssGJU<$(3rH2t_z+>tM2}h+x??WcfA6yGWU`a@IRw8X%tT*3n6?+ZrN9EAY)XIa_#bxZUfdt@L5OU$3B&0%cG7Kku za1A8yUCoDWAb^6wE>_s{P%x|m_4L||(>=AXB(p2%L-~ zUV*@#5iC0}#T8?8Rt5RIHhGZ1)j=+>N(z6%3Q;89z000BsNklb0H7{`C-%(=J7lrp8mYZY4B4y`SPhKVJG7{TDe6>AfG`38Il6XVjjB~fDH z!bCT&v^xM6ojK2OaqryDNH;D#$&=@Q{`cm2Kj*;zwA2Tm ze*Ka0w)Hg`gO^C)X^n3TUZPR^CIJpA6-y9UD%D6w`yjAX0!vV_R5pSo-H0Iz${U(qi2vCt$0BSP^rw-CucYxR)RZP)ea{#gDL3Dv~e@5r>FG5@H+g z1+5e0CMOrn?p-N4($z`1Tz;jq`NC3NuJua;8U-YDD7DqzzKfMV7OQ4QON-i>O0h#^ zOT$A$Z{qtN*RM}-^k@dpGnC64OikURzrUMYZjPRwg8&o?i?p`3kxVvg0|WqC>n-Qe z^E`at2f%M?!uLI_gc&q7MZE8A&aYNjYuAm41OQjA+}P6a*w{pj$8X+k_;%~o6acy0 zy_gdbD%EOPL)p{_8!k&z()1_pY_W(NT1?K{k|p+1iE zb^?&e97HKaN9TT4n;wuW-Q@D`m-uG%@^C}M^M!njuU)$hz~tm+7BkaD*2>Q*t95q! z`^e`P!v-(?#@MrKOjK^N_;iW2hZ`LY5$|n(ojnH*05CY13A5;Ir#Fgldpbd>?J@IA z@zdCkq*}L=FXgyx$0#Io+^6Mr(ND$vA($woFIYVHW0p z<-6`VLrw;ZcG9&JG-#y_}LDw&i=sSaFPD^ z2MIPREHxLIo1dfdz2V%RcR7FlJkvANk7L8x>;Tr<@KUi7V{w95!8u2V>?bwX%ole* zgLfQzJNIzExWMXHC5H7W&YU@e@eJv0Rei=HI{l1O7V! UkNeER#{d8T07*qoM6N<$f+3;*#{d8T literal 0 HcmV?d00001 diff --git a/templates/default/images/messages/permission_error22.png b/templates/default/images/messages/permission_error22.png new file mode 100755 index 0000000000000000000000000000000000000000..c0947f07972b86a2a2cd57162af0b35663a1375b GIT binary patch literal 4048 zcmV;>4=?bEP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}000E*NklywB|QK>%I+gpjy2cM)qv@g{V6`~JUEx2H_*2Ct%E_>~@!a@I&h|U$eC($3H9T|@fv8yxD2aVF) zB(E32p}>iHX*LEHqxVR7Ke%K0d}%}QgFU^Sn`-xcp6xps99X_IGsEdqmj3X2FV8eC zUy2^jd9LYw;2+%tEvWPc&wQ~KzB=^nhk{+ZsZ=Q=?G_;+OG#5&Ono1*|&P4a7k#i*rfsC@jFP1VoQ?zKNPR*sZUq~nms)3g>@r~T~dvd?K zn0X}zX)7{P64{q5vVJCTF)$gJkJ=5!@_;C9CEjdz*gl?+Mt#mNqv1*GnXO1s)r8@FRHBt|h(b=So z{xXMc7;3XB2~Tn;m)k$>U^I6Qg~)w{AWLE!tK=%@BL^N)@|;HE*1<$8W~3# z78XjII+zNNtPG(=zPu&%{I{x-F1!=z2vkZMC23piL{}p8))MuiG=UMz89dii{;Tj{ zjh_dkcdd-fk~0(0;^orbTT;o+lzyyFS+{o7=;jS!Wc`}3s=rrPJJ;39jUe>>h+`5geTUo!3k=69ABGSA*;b?KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}000EINkl&=H9vUxYHS?wXd{PDGx;? zEwqg_#sXCujYiTKYcPTlKb*#o8mWnx#`K2|ifQ482pS+jG%*cNW5Pp`YDmK#bTeewzf_@Fqw$vok%1icXV`|9T^!RBBFRaPNSovG&VLy$z+lei3Iib z_D;98wQXG}pNJqVRIIqT_+%s!*`#S24j(z%KOUVpV_DX3X+53ecFT1e>Ym$FTl;j# z>-B!#($W&&vuDqlMHz3|vZblJyPGB_C!MaYeeDaedHs#8W#`VF9np22+S}WQ8yg$B zxiQ?_LWW^PR8_^XW5=&VBJX{?(2tHCJ$&=jsnc%{4i1_e$32!vB%YlclLfqO+jg<8 zZo|@WI1JmiPv9Z8fB*i=vDja+>Z&TPprD|7u0KoTovj}+m*k>E{0?5&+~8ml3oQv{ za&mG|QBmP~DC3l-K~L$>G!440bBh7%n$8^Ch8hUaLxC&St}zTFO|!GJuq ziuOq@vRaG~q4MPd|{R81}__ipDkR%DBC_)rPFvjk76M(Us zH#hJ+uLc5vAyE`hDvGiTfRhE>+1c3{i^ZCE?AQS!LNb{|C=`M$%a9}qvMfUo1aMro zLVbOG7#J7;&+}SoX=y=yef>XKQlwHTQWOPcWn~Bi0&pA$nx-L>$-p#Cn5Ma)9MjX& zh(@EBnVHEa@ig2CYY zb|cOW%aNe#02mKNks$~k7^VWjRN(i^kYyhDPsDqSJRUb(k_=G}f|u0!z~zAfG3X>< z8$Ia#c>-7aM^L+-$NFl(F(iEbl@Bv133W9lR;~ivvOhQgJEiQgrB+UdoLM@@WpAcb&!kz5A+6ycnj9&!}XKxQJpd+TN&=hp5uEuny4z#o%5_8v?DNgZ9y8PqHM2TH0KI zM%ed38kMWk`1U6mySiKe5e{uLQ2vOHJ1HLLe^utM&4z}Cy zrDd6a5|JbLd_L8-?Y#2xv|OtD`bqk9LI4y&x0`z z9LI3?&HmpNL~tAeKsLZ;ZQC9d1pb;J2-g^6!~YHd_x*YtO+u6$00000NkvXXu0mjf DWV>yG literal 0 HcmV?d00001 diff --git a/templates/default/images/mode_sprites.png b/templates/default/images/mode_sprites.png new file mode 100755 index 0000000000000000000000000000000000000000..cff1010aab0782ceb9f5291f572a2f0e94b238a6 GIT binary patch literal 4063 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 Ry(s_y002ovPDHLkV1jH5{`~*| literal 0 HcmV?d00001 diff --git a/templates/default/images/slideknob.png b/templates/default/images/slideknob.png new file mode 100755 index 0000000000000000000000000000000000000000..2e2731b5dee2bfe0b23f088c9f546549601c50cf GIT binary patch literal 584 zcmV-O0=NB%P)Ca_1}b`PpgStKMG1IL{ry?w8q+sn;C>h;9T_}-wV4urXa2PGe~>VXIqwF=vEu-q_K zEQ|?3$n&{=uYYbjZy(jUGl_+zEjy1542}6ak`9rujkYzGt??Gac%1Tl^5yh*`~Nde3ON!^|n!( zNU6l9M^ERas=mE=`+zBH;PqVY`!eYIsd(2}em;%Wj>R9a9N8f8_-P8V+NpZQty z$04nBo$SnH<~x>3isl9f*U{+QA`6+hJB1tjdVc4Zy~O34M=p+zO+K!RIqa@BWYX_O zoaDB%<+ye^5J1HnPP`p2jd}TOm5w*IFJ9Xfl;!>F8>5%f2gNgPv7`0jUk7dUoxcJ1 W=;38%9SX7l0000v)e5ZBQx4|Y-Q?nr@Px3?9h(3ZWr3^tj=`TP57gKr87N$ zp2wWee1GRRCwo_xahnw)5cxNPJbCg2L6DV|6`#+yw6v6!mDS$f9-JvFD^n;GQ&UrZ zzh5jCkByB101O60U0q#p_1BM>Cv-vP?&s4@g_((4_1L=L$(a91)0=J91Gas#R{McE znYG^9*0A5YZ>#;~+Wkn(W5B0^yELIYLP!K}mB~<)AM@1&nqekynuaEGqPrzoH|KodRXJy)%+w_fu3nE5>@Bd_b zqC$EQ;{c`T&?EsNO|igL9gC7Ygxv?aQUEXMq?~>wg{EyW;VcJ37CUF#HjrT=KQO_* zS>M9yydXk18D(+QDJ1>r);Lav_uYKp$T?4vr{Q$lTo&pKv^?(>L-)G2*lwH!Ah7k? z7oH<8h-(KTKt5V6$8gF)C7Io&P5=SjTh)=zV=E2EUhQZP##L8S{d%UK>>+y82>+FV+#^BzW7u3F)Bb>=lYQ%%j`F>ASe zo*cw@V#u6T`A2He;70mR(V&iV&-7{qP~=SRf&jm9-T{*ZeZ}$rd0#6c&fLG^xJcf5 z+p<`wJYgW+_s*V{uI$nMB;%8`S_3>PfGOj3Rq}@Cx^+j?rk92fANSFDBYnOqQ>Vdj z)(|$AhP4t&Lb=Gvo2#3Gl%9<=Gv`Mz?Po@P4iLF!x}GUWJICDlFk-hS^Whyh7x~VH z@0vD1>HYD4&e+~yzS*-sFR{9`{QEEZO1zg7>R&7cHts-6j!xHVdA8eI+ZlVzd%`es zJT@$#GX(gvCJ1oJN%yLBK}{V=V;seo;!w|Yte!W1%5qLNFWqvZW>h&IiH+oPT=b@E zPhGzv5=(Un*X>v`>%8h_nj^NdYcE6NHS_ifkCV$*D)Tqrbu`s;<=t<4 zAHNqNV?6(g<1PY-w@#I-WYFViz?9TrkMr)u0g`O`u|>T;k|2sV*YF^punvT;$SuTy{j3Gv)yqD!R_CF>yR)MzmmYS5v+~R zXAdD%ng9?df;wd8GxR#%3O+gz};Vo;)sK%Bj-q>Oq%R7JU-KD?vYu>#2UjaDo z&8$>5xW~?KPD_#XFToU1hIb*VOMidUr6iYiO0N|i-7s`T8!cFT`rN!^1Pt78J93i6 z5HI1wIM$94m{3SLDvISDe6$ZG1;eq_D9RTaaC>=cO{@Bs>$IlPCPJJ$h$)-3vzNUQ6OsN#_zWxey!_9%hxwH2_dEJi=yY|1c7nDm2_Lm!Cof8-R_+9UkS zcBE(o47yE)oMR(Q=dp1a2wTX5KvvGyLqlWTa7V&!A*|w|)ax~1_~aJ0=_Lilg*0iQk7#ZD EAHN$8j{pDw literal 0 HcmV?d00001 diff --git a/templates/default/images/tree_closed-hover.png b/templates/default/images/tree_closed-hover.png new file mode 100755 index 0000000000000000000000000000000000000000..03140b204725836732578226f8b267e9b57e149d GIT binary patch literal 418 zcmV;T0bTxyP)Hd=PTFSn~n(iI_d#$P}RDw$AUI{>$0Y?y8 zK#QfN6^_2ayNxPzI-RF-??xO!YX+qoTa;qjhqrivr-li#tzzpc><2-{iq(f|Me M07*qoM6N<$f~n=O3;+NC literal 0 HcmV?d00001 diff --git a/templates/default/images/tree_closed.png b/templates/default/images/tree_closed.png new file mode 100755 index 0000000000000000000000000000000000000000..0a0458b135cbb89113ffd31582b0f855487813fc GIT binary patch literal 434 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhEa{HEjtmSN`?>!lvI6;RN#5=* z4F5rJ!QSPQfg+p*9+AaBUBV#D$S`Y;1Oo%3lc$Sgh{R>ziH7T>90gqB&+~BK%3y9$ zwKkljT(piMev|7V-yIy9N)H%h8X8&}xGjFVEa~XrShD}#%$H>wzP;KWlC?GD)w@+n z$4)#gG7LR>=7i=JMH&SRiYr#3y= z+^ebDlqOOC-!V|+&!4Zywy%$85e#5%2@74#y_Nr-{(jwMQk##s|8?^HDJ*?ujraL8 aKe*+-ZL)lSqN5xb#0;LUelF{r5}E)~_q4JA literal 0 HcmV?d00001 diff --git a/templates/default/images/tree_open-hover.png b/templates/default/images/tree_open-hover.png new file mode 100755 index 0000000000000000000000000000000000000000..a874af00549220bc05a95328e09ae3bee6f5a9a4 GIT binary patch literal 425 zcmV;a0apHrP)n+oyv$M3+O49fn z0(KTQvLK7F&g{H*kHt8OHpI8;+l4yO;rTupLnTXRh3U;7IdsYXxv zare{ex&|P0IfDmMN-XE}&*4B|@dn1@aUWj-mrxOs=(>)*7ZTHzp1hRYol**taQD7T zNq>b8pCzW>_5PDI=%tj0a4CiLdd;g#sc9P8w*3z_NdV-Wi821mM_<=qWM=#ZsxipO TGYL#;00000NkvXXu0mjfndP;I literal 0 HcmV?d00001 diff --git a/templates/default/images/tree_open.png b/templates/default/images/tree_open.png new file mode 100755 index 0000000000000000000000000000000000000000..44c4290de5fed2f5851bbc3a067618d219bf4346 GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhEa{HEjtmSN`?>!lvI6;RN#5=* z4F5rJ!QSPQfg+p*9+AaBUBV#D$S`Y;1Oo%3wWo_?h{R>zi3hu+0tHFo zacD=8amNF%JDiV}@v3w@c;6QJOuWrO;U|~z#@(wnWn5aa?fbprm0E`Xd$V0N96~j_ zn(C9&W=I;k?OLb2Z-4#w?d$LV`1zV$Q&p~?Jv8)UsjOxCnVzdzu9r<(u3ztcGKVqa z_OwkpfuRfXgF=hW<(NrxZtSS}_o?${jvEJKsOnM|FV6CG8@b}$cLf9w9iPu1rtf_- zZ~G2wL$@8|SPFqr9MG1pH?$#&;ks|^P- zOcrHIty*>HuAtz#=fjoFOe3H;hE=6djN z-SN9SPA3|xE8D&{wX7dRlP!t7bMo|<142MJG z6Oy09NBjQm2fu!gNRout*H7PwOVw$T{@kPTwh~!?-`{h2d1((15AP1R$=?aBWdKy;p5_N!npQ!CgM$O9)oK()K?ng6 zVR3Phy}dmVkvVZ3$IQHQ4ppTrOI}`HxV^pQ?(WV2Tv3*wj%u|^x7(#ssbFS^2>bi{ z)M_;V%%(7t$pq&dd7cBHs-$U3yIoZ@4xTPG|O|AW$f(i zaB*=V+uPgkVi&(PX2$aJvTST@Si9YpJkOa$ai;c{uXyh{IzD7&W#z*h&s#QYn31){y=arguments; +}else{if(v){y=[x];}}}if(y){w={};for(var z=0;z>>0; +b>>0;b>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a>>0,b=Array(d);for(var a=0;a>>0; +b-1:String(this).indexOf(a)>-1;},trim:function(){return String(this).replace(/^\s+|\s+$/g,""); +},clean:function(){return String(this).replace(/\s+/g," ").trim();},camelCase:function(){return String(this).replace(/-\D/g,function(a){return a.charAt(1).toUpperCase(); +});},hyphenate:function(){return String(this).replace(/[A-Z]/g,function(a){return("-"+a.charAt(0).toLowerCase());});},capitalize:function(){return String(this).replace(/\b[a-z]/g,function(a){return a.toUpperCase(); +});},escapeRegExp:function(){return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},toInt:function(a){return parseInt(this,a||10);},toFloat:function(){return parseFloat(this); +},hexToRgb:function(b){var a=String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);return(a)?a.slice(1).hexToRgb(b):null;},rgbToHex:function(b){var a=String(this).match(/\d{1,3}/g); +return(a)?a.rgbToHex(b):null;},substitute:function(a,b){return String(this).replace(b||(/\\?\{([^{}]+)\}/g),function(d,c){if(d.charAt(0)=="\\"){return d.slice(1); +}return(a[c]!=null)?a[c]:"";});}});Number.implement({limit:function(b,a){return Math.min(a,Math.max(b,this));},round:function(a){a=Math.pow(10,a||0).toFixed(a<0?-a:0); +return Math.round(this*a)/a;},times:function(b,c){for(var a=0;a1?Array.slice(arguments,1):null,d=function(){};var c=function(){var g=e,h=arguments.length;if(this instanceof c){d.prototype=a.prototype; +g=new d;}var f=(!b&&!h)?a.call(g):a.apply(g,b&&h?b.concat(Array.slice(arguments)):b||arguments);return g==e?f:g;};return c;},pass:function(b,c){var a=this; +if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},delay:function(b,c,a){return setTimeout(this.pass((a==null?[]:a),c),b); +},periodical:function(c,b,a){return setInterval(this.pass((a==null?[]:a),b),c);}});delete Function.prototype.bind;Function.implement({create:function(b){var a=this; +b=b||{};return function(d){var c=b.arguments;c=(c!=null)?Array.from(c):Array.slice(arguments,(b.event)?1:0);if(b.event){c=[d||window.event].extend(c);}var e=function(){return a.apply(b.bind||null,c); +};if(b.delay){return setTimeout(e,b.delay);}if(b.periodical){return setInterval(e,b.periodical);}if(b.attempt){return Function.attempt(e);}return e();}; +},bind:function(c,b){var a=this;if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},bindWithEvent:function(c,b){var a=this; +if(b!=null){b=Array.from(b);}return function(d){return a.apply(c,(b==null)?arguments:[d].concat(b));};},run:function(a,b){return this.apply(b,Array.from(a)); +}});if(Object.create==Function.prototype.create){Object.create=null;}var $try=Function.attempt;(function(){var a=Object.prototype.hasOwnProperty;Object.extend({subset:function(d,g){var f={}; +for(var e=0,b=g.length;e]*>([\s\S]*?)<\/script>/gi,function(r,s){e+=s+"\n"; +return"";});if(p===true){o.exec(e);}else{if(typeOf(p)=="function"){p(e,q);}}return q;});o.extend({Document:this.Document,Window:this.Window,Element:this.Element,Event:this.Event}); +this.Window=this.$constructor=new Type("Window",function(){});this.$family=Function.from("window").hide();Window.mirror(function(e,p){h[e]=p;});this.Document=k.$constructor=new Type("Document",function(){}); +k.$family=Function.from("document").hide();Document.mirror(function(e,p){k[e]=p;});k.html=k.documentElement;if(!k.head){k.head=k.getElementsByTagName("head")[0]; +}if(k.execCommand){try{k.execCommand("BackgroundImageCache",false,true);}catch(g){}}if(this.attachEvent&&!this.addEventListener){var c=function(){this.detachEvent("onunload",c); +k.head=k.html=k.window=null;};this.attachEvent("onunload",c);}var m=Array.from;try{m(k.html.childNodes);}catch(g){Array.from=function(p){if(typeof p!="string"&&Type.isEnumerable(p)&&typeOf(p)!="array"){var e=p.length,q=new Array(e); +while(e--){q[e]=p[e];}return q;}return m(p);};var l=Array.prototype,n=l.slice;["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice"].each(function(e){var p=l[e]; +Array[e]=function(q){return p.apply(Array.from(q),n.call(arguments,1));};});}if(o.Platform.ios){o.Platform.ipod=true;}o.Engine={};var d=function(p,e){o.Engine.name=p; +o.Engine[p+e]=true;o.Engine.version=e;};if(o.ie){o.Engine.trident=true;switch(o.version){case 6:d("trident",4);break;case 7:d("trident",5);break;case 8:d("trident",6); +}}if(o.firefox){o.Engine.gecko=true;if(o.version>=3){d("gecko",19);}else{d("gecko",18);}}if(o.safari||o.chrome){o.Engine.webkit=true;switch(o.version){case 2:d("webkit",419); +break;case 3:d("webkit",420);break;case 4:d("webkit",525);}}if(o.opera){o.Engine.presto=true;if(o.version>=9.6){d("presto",960);}else{if(o.version>=9.5){d("presto",950); +}else{d("presto",925);}}}if(o.name=="unknown"){switch((a.match(/(?:webkit|khtml|gecko)/)||[])[0]){case"webkit":case"khtml":o.Engine.webkit=true;break;case"gecko":o.Engine.gecko=true; +}}this.$exec=o.exec;})();(function(){var b={};var a=this.DOMEvent=new Type("DOMEvent",function(c,g){if(!g){g=window;}c=c||g.event;if(c.$extended){return c; +}this.event=c;this.$extended=true;this.shift=c.shiftKey;this.control=c.ctrlKey;this.alt=c.altKey;this.meta=c.metaKey;var i=this.type=c.type;var h=c.target||c.srcElement; +while(h&&h.nodeType==3){h=h.parentNode;}this.target=document.id(h);if(i.indexOf("key")==0){var d=this.code=(c.which||c.keyCode);this.key=b[d]||Object.keyOf(Event.Keys,d); +if(i=="keydown"){if(d>111&&d<124){this.key="f"+(d-111);}else{if(d>95&&d<106){this.key=d-96;}}}if(this.key==null){this.key=String.fromCharCode(d).toLowerCase(); +}}else{if(i=="click"||i=="dblclick"||i=="contextmenu"||i=="DOMMouseScroll"||i.indexOf("mouse")==0){var j=g.document;j=(!j.compatMode||j.compatMode=="CSS1Compat")?j.html:j.body; +this.page={x:(c.pageX!=null)?c.pageX:c.clientX+j.scrollLeft,y:(c.pageY!=null)?c.pageY:c.clientY+j.scrollTop};this.client={x:(c.pageX!=null)?c.pageX-g.pageXOffset:c.clientX,y:(c.pageY!=null)?c.pageY-g.pageYOffset:c.clientY}; +if(i=="DOMMouseScroll"||i=="mousewheel"){this.wheel=(c.wheelDelta)?c.wheelDelta/120:-(c.detail||0)/3;}this.rightClick=(c.which==3||c.button==2);if(i=="mouseover"||i=="mouseout"){var k=c.relatedTarget||c[(i=="mouseover"?"from":"to")+"Element"]; +while(k&&k.nodeType==3){k=k.parentNode;}this.relatedTarget=document.id(k);}}else{if(i.indexOf("touch")==0||i.indexOf("gesture")==0){this.rotation=c.rotation; +this.scale=c.scale;this.targetTouches=c.targetTouches;this.changedTouches=c.changedTouches;var f=this.touches=c.touches;if(f&&f[0]){var e=f[0];this.page={x:e.pageX,y:e.pageY}; +this.client={x:e.clientX,y:e.clientY};}}}}if(!this.client){this.client={};}if(!this.page){this.page={};}});a.implement({stop:function(){return this.preventDefault().stopPropagation(); +},stopPropagation:function(){if(this.event.stopPropagation){this.event.stopPropagation();}else{this.event.cancelBubble=true;}return this;},preventDefault:function(){if(this.event.preventDefault){this.event.preventDefault(); +}else{this.event.returnValue=false;}return this;}});a.defineKey=function(d,c){b[d]=c;return this;};a.defineKeys=a.defineKey.overloadSetter(true);a.defineKeys({"38":"up","40":"down","37":"left","39":"right","27":"esc","32":"space","8":"backspace","9":"tab","46":"delete","13":"enter"}); +})();var Event=DOMEvent;Event.Keys={};Event.Keys=new Hash(Event.Keys);(function(){var a=this.Class=new Type("Class",function(h){if(instanceOf(h,Function)){h={initialize:h}; +}var g=function(){e(this);if(g.$prototyping){return this;}this.$caller=null;var i=(this.initialize)?this.initialize.apply(this,arguments):this;this.$caller=this.caller=null; +return i;}.extend(this).implement(h);g.$constructor=a;g.prototype.$constructor=g;g.prototype.parent=c;return g;});var c=function(){if(!this.$caller){throw new Error('The method "parent" cannot be called.'); +}var g=this.$caller.$name,h=this.$caller.$owner.parent,i=(h)?h.prototype[g]:null;if(!i){throw new Error('The method "'+g+'" has no parent.');}return i.apply(this,arguments); +};var e=function(g){for(var h in g){var j=g[h];switch(typeOf(j)){case"object":var i=function(){};i.prototype=j;g[h]=e(new i);break;case"array":g[h]=j.clone(); +break;}}return g;};var b=function(g,h,j){if(j.$origin){j=j.$origin;}var i=function(){if(j.$protected&&this.$caller==null){throw new Error('The method "'+h+'" cannot be called.'); +}var l=this.caller,m=this.$caller;this.caller=m;this.$caller=i;var k=j.apply(this,arguments);this.$caller=m;this.caller=l;return k;}.extend({$owner:g,$origin:j,$name:h}); +return i;};var f=function(h,i,g){if(a.Mutators.hasOwnProperty(h)){i=a.Mutators[h].call(this,i);if(i==null){return this;}}if(typeOf(i)=="function"){if(i.$hidden){return this; +}this.prototype[h]=(g)?i:b(this,h,i);}else{Object.merge(this.prototype,h,i);}return this;};var d=function(g){g.$prototyping=true;var h=new g;delete g.$prototyping; +return h;};a.implement("implement",f.overloadSetter());a.Mutators={Extends:function(g){this.parent=g;this.prototype=d(g);},Implements:function(g){Array.from(g).each(function(j){var h=new j; +for(var i in h){f.call(this,i,h[i],true);}},this);}};})();(function(){this.Chain=new Class({$chain:[],chain:function(){this.$chain.append(Array.flatten(arguments)); +return this;},callChain:function(){return(this.$chain.length)?this.$chain.shift().apply(this,arguments):false;},clearChain:function(){this.$chain.empty(); +return this;}});var a=function(b){return b.replace(/^on([A-Z])/,function(c,d){return d.toLowerCase();});};this.Events=new Class({$events:{},addEvent:function(d,c,b){d=a(d); +if(c==$empty){return this;}this.$events[d]=(this.$events[d]||[]).include(c);if(b){c.internal=true;}return this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]); +}return this;},fireEvent:function(e,c,b){e=a(e);var d=this.$events[e];if(!d){return this;}c=Array.from(c);d.each(function(f){if(b){f.delay(b,this,c);}else{f.apply(this,c); +}},this);return this;},removeEvent:function(e,d){e=a(e);var c=this.$events[e];if(c&&!d.internal){var b=c.indexOf(d);if(b!=-1){delete c[b];}}return this; +},removeEvents:function(d){var e;if(typeOf(d)=="object"){for(e in d){this.removeEvent(e,d[e]);}return this;}if(d){d=a(d);}for(e in this.$events){if(d&&d!=e){continue; +}var c=this.$events[e];for(var b=c.length;b--;){if(b in c){this.removeEvent(e,c[b]);}}}return this;}});this.Options=new Class({setOptions:function(){var b=this.options=Object.merge.apply(null,[{},this.options].append(arguments)); +if(this.addEvent){for(var c in b){if(typeOf(b[c])!="function"||!(/^on[A-Z]/).test(c)){continue;}this.addEvent(c,b[c]);delete b[c];}}return this;}});})(); +(function(){var k,n,l,g,a={},c={},m=/\\/g;var e=function(q,p){if(q==null){return null;}if(q.Slick===true){return q;}q=(""+q).replace(/^\s+|\s+$/g,"");g=!!p; +var o=(g)?c:a;if(o[q]){return o[q];}k={Slick:true,expressions:[],raw:q,reverse:function(){return e(this.raw,true);}};n=-1;while(q!=(q=q.replace(j,b))){}k.length=k.expressions.length; +return o[k.raw]=(g)?h(k):k;};var i=function(o){if(o==="!"){return" ";}else{if(o===" "){return"!";}else{if((/^!/).test(o)){return o.replace(/^!/,"");}else{return"!"+o; +}}}};var h=function(u){var r=u.expressions;for(var p=0;p+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(//,"["+f(">+~`!@$%^&={}\\;/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(//g,"(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])")); +function b(x,s,D,z,r,C,q,B,A,y,u,F,G,v,p,w){if(s||n===-1){k.expressions[++n]=[];l=-1;if(s){return"";}}if(D||z||l===-1){D=D||" ";var t=k.expressions[n]; +if(g&&t[l]){t[l].reverseCombinator=i(D);}t[++l]={combinator:D,tag:"*"};}var o=k.expressions[n][l];if(r){o.tag=r.replace(m,"");}else{if(C){o.id=C.replace(m,""); +}else{if(q){q=q.replace(m,"");if(!o.classList){o.classList=[];}if(!o.classes){o.classes=[];}o.classList.push(q);o.classes.push({value:q,regexp:new RegExp("(^|\\s)"+f(q)+"(\\s|$)")}); +}else{if(G){w=w||p;w=w?w.replace(m,""):null;if(!o.pseudos){o.pseudos=[];}o.pseudos.push({key:G.replace(m,""),value:w,type:F.length==1?"class":"element"}); +}else{if(B){B=B.replace(m,"");u=(u||"").replace(m,"");var E,H;switch(A){case"^=":H=new RegExp("^"+f(u));break;case"$=":H=new RegExp(f(u)+"$");break;case"~=":H=new RegExp("(^|\\s)"+f(u)+"(\\s|$)"); +break;case"|=":H=new RegExp("^"+f(u)+"(-|$)");break;case"=":E=function(I){return u==I;};break;case"*=":E=function(I){return I&&I.indexOf(u)>-1;};break; +case"!=":E=function(I){return u!=I;};break;default:E=function(I){return !!I;};}if(u==""&&(/^[*$^]=$/).test(A)){E=function(){return false;};}if(!E){E=function(I){return I&&H.test(I); +};}if(!o.attributes){o.attributes=[];}o.attributes.push({key:B,operator:A,value:u,test:E});}}}}}return"";}var d=(this.Slick||{});d.parse=function(o){return e(o); +};d.escapeRegExp=f;if(!this.Slick){this.Slick=d;}}).apply((typeof exports!="undefined")?exports:this);(function(){var k={},m={},d=Object.prototype.toString; +k.isNativeCode=function(c){return(/\{\s*\[native code\]\s*\}/).test(""+c);};k.isXML=function(c){return(!!c.xmlVersion)||(!!c.xml)||(d.call(c)=="[object XMLDocument]")||(c.nodeType==9&&c.documentElement.nodeName!="HTML"); +};k.setDocument=function(w){var p=w.nodeType;if(p==9){}else{if(p){w=w.ownerDocument;}else{if(w.navigator){w=w.document;}else{return;}}}if(this.document===w){return; +}this.document=w;var A=w.documentElement,o=this.getUIDXML(A),s=m[o],r;if(s){for(r in s){this[r]=s[r];}return;}s=m[o]={};s.root=A;s.isXMLDocument=this.isXML(w); +s.brokenStarGEBTN=s.starSelectsClosedQSA=s.idGetsName=s.brokenMixedCaseQSA=s.brokenGEBCN=s.brokenCheckedQSA=s.brokenEmptyAttributeQSA=s.isHTMLDocument=s.nativeMatchesSelector=false; +var q,u,y,z,t;var x,v="slick_uniqueid";var c=w.createElement("div");var n=w.body||w.getElementsByTagName("body")[0]||A;n.appendChild(c);try{c.innerHTML=''; +s.isHTMLDocument=!!w.getElementById(v);}catch(C){}if(s.isHTMLDocument){c.style.display="none";c.appendChild(w.createComment(""));u=(c.getElementsByTagName("*").length>1); +try{c.innerHTML="foo";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML=''; +s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='';c.getElementsByClassName("b").length; +c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='';y=(c.getElementsByClassName("a").length!=2); +}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/"); +}catch(C){}try{c.innerHTML='';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML=''; +s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0); +}catch(C){}}try{c.innerHTML='
    ';t=(c.firstChild.getAttribute("action")!="s");}catch(C){}s.nativeMatchesSelector=A.matchesSelector||A.mozMatchesSelector||A.webkitMatchesSelector; +if(s.nativeMatchesSelector){try{s.nativeMatchesSelector.call(A,":slick");s.nativeMatchesSelector=null;}catch(C){}}}try{A.slick_expando=1;delete A.slick_expando; +s.getUID=this.getUIDHTML;}catch(C){s.getUID=this.getUIDXML;}n.removeChild(c);c=x=n=null;s.getAttribute=(s.isHTMLDocument&&t)?function(G,E){var H=this.attributeGetters[E]; +if(H){return H.call(G);}var F=G.getAttributeNode(E);return(F)?F.nodeValue:null;}:function(F,E){var G=this.attributeGetters[E];return(G)?G.call(F):F.getAttribute(E); +};s.hasAttribute=(A&&this.isNativeCode(A.hasAttribute))?function(F,E){return F.hasAttribute(E);}:function(F,E){F=F.getAttributeNode(E);return !!(F&&(F.specified||F.nodeValue)); +};var D=A&&this.isNativeCode(A.contains),B=w&&this.isNativeCode(w.contains);s.contains=(D&&B)?function(E,F){return E.contains(F);}:(D&&!B)?function(E,F){return E===F||((E===w)?w.documentElement:E).contains(F); +}:(A&&A.compareDocumentPosition)?function(E,F){return E===F||!!(E.compareDocumentPosition(F)&16);}:function(E,F){if(F){do{if(F===E){return true;}}while((F=F.parentNode)); +}return false;};s.documentSorter=(A.compareDocumentPosition)?function(F,E){if(!F.compareDocumentPosition||!E.compareDocumentPosition){return 0;}return F.compareDocumentPosition(E)&4?-1:F===E?0:1; +}:("sourceIndex" in A)?function(F,E){if(!F.sourceIndex||!E.sourceIndex){return 0;}return F.sourceIndex-E.sourceIndex;}:(w.createRange)?function(H,F){if(!H.ownerDocument||!F.ownerDocument){return 0; +}var G=H.ownerDocument.createRange(),E=F.ownerDocument.createRange();G.setStart(H,0);G.setEnd(H,0);E.setStart(F,0);E.setEnd(F,0);return G.compareBoundaryPoints(Range.START_TO_END,E); +}:null;A=null;for(r in s){this[r]=s[r];}};var f=/^([#.]?)((?:[\w-]+|\*))$/,h=/\[.+[*$^]=(?:""|'')?\]/,g={};k.search=function(U,z,H,s){var p=this.found=(s)?null:(H||[]); +if(!U){return p;}else{if(U.navigator){U=U.document;}else{if(!U.nodeType){return p;}}}var F,O,V=this.uniques={},I=!!(H&&H.length),y=(U.nodeType==9);if(this.document!==(y?U:U.ownerDocument)){this.setDocument(U); +}if(I){for(O=p.length;O--;){V[this.getUID(p[O])]=true;}}if(typeof z=="string"){var r=z.match(f);simpleSelectors:if(r){var u=r[1],v=r[2],A,E;if(!u){if(v=="*"&&this.brokenStarGEBTN){break simpleSelectors; +}E=U.getElementsByTagName(v);if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{if(u=="#"){if(!this.isHTMLDocument||!y){break simpleSelectors; +}A=U.getElementById(v);if(!A){return p;}if(this.idGetsName&&A.getAttributeNode("id").nodeValue!=v){break simpleSelectors;}if(s){return A||null;}if(!(I&&V[this.getUID(A)])){p.push(A); +}}else{if(u=="."){if(!this.isHTMLDocument||((!U.getElementsByClassName||this.brokenGEBCN)&&U.querySelectorAll)){break simpleSelectors;}if(U.getElementsByClassName&&!this.brokenGEBCN){E=U.getElementsByClassName(v); +if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{var T=new RegExp("(^|\\s)"+e.escapeRegExp(v)+"(\\s|$)");E=U.getElementsByTagName("*"); +for(O=0;A=E[O++];){className=A.className;if(!(className&&T.test(className))){continue;}if(s){return A;}if(!(I&&V[this.getUID(A)])){p.push(A);}}}}}}if(I){this.sort(p); +}return(s)?null:p;}querySelector:if(U.querySelectorAll){if(!this.isHTMLDocument||g[z]||this.brokenMixedCaseQSA||(this.brokenCheckedQSA&&z.indexOf(":checked")>-1)||(this.brokenEmptyAttributeQSA&&h.test(z))||(!y&&z.indexOf(",")>-1)||e.disableQSA){break querySelector; +}var S=z,x=U;if(!y){var C=x.getAttribute("id"),t="slickid__";x.setAttribute("id",t);S="#"+t+" "+S;U=x.parentNode;}try{if(s){return U.querySelector(S)||null; +}else{E=U.querySelectorAll(S);}}catch(Q){g[z]=1;break querySelector;}finally{if(!y){if(C){x.setAttribute("id",C);}else{x.removeAttribute("id");}U=x;}}if(this.starSelectsClosedQSA){for(O=0; +A=E[O++];){if(A.nodeName>"@"&&!(I&&V[this.getUID(A)])){p.push(A);}}}else{for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}if(I){this.sort(p); +}return p;}F=this.Slick.parse(z);if(!F.length){return p;}}else{if(z==null){return p;}else{if(z.Slick){F=z;}else{if(this.contains(U.documentElement||U,z)){(p)?p.push(z):p=z; +return p;}else{return p;}}}}this.posNTH={};this.posNTHLast={};this.posNTHType={};this.posNTHTypeLast={};this.push=(!I&&(s||(F.length==1&&F.expressions[0].length==1)))?this.pushArray:this.pushUID; +if(p==null){p=[];}var M,L,K;var B,J,D,c,q,G,W;var N,P,o,w,R=F.expressions;search:for(O=0;(P=R[O]);O++){for(M=0;(o=P[M]);M++){B="combinator:"+o.combinator; +if(!this[B]){continue search;}J=(this.isXMLDocument)?o.tag:o.tag.toUpperCase();D=o.id;c=o.classList;q=o.classes;G=o.attributes;W=o.pseudos;w=(M===(P.length-1)); +this.bitUniques={};if(w){this.uniques=V;this.found=p;}else{this.uniques={};this.found=[];}if(M===0){this[B](U,J,D,q,G,W,c);if(s&&w&&p.length){break search; +}}else{if(s&&w){for(L=0,K=N.length;L1)){this.sort(p);}return(s)?(p[0]||null):p;};k.uidx=1;k.uidk="slick-uniqueid";k.getUIDXML=function(n){var c=n.getAttribute(this.uidk); +if(!c){c=this.uidx++;n.setAttribute(this.uidk,c);}return c;};k.getUIDHTML=function(c){return c.uniqueNumber||(c.uniqueNumber=this.uidx++);};k.sort=function(c){if(!this.documentSorter){return c; +}c.sort(this.documentSorter);return c;};k.cacheNTH={};k.matchNTH=/^([+-]?\d*)?([a-z]+)?([+-]\d+)?$/;k.parseNTHArgument=function(q){var o=q.match(this.matchNTH); +if(!o){return false;}var p=o[2]||false;var n=o[1]||1;if(n=="-"){n=-1;}var c=+o[3]||0;o=(p=="n")?{a:n,b:c}:(p=="odd")?{a:2,b:1}:(p=="even")?{a:2,b:0}:{a:0,b:n}; +return(this.cacheNTH[q]=o);};k.createNTHPseudo=function(p,n,c,o){return function(s,q){var u=this.getUID(s);if(!this[c][u]){var A=s.parentNode;if(!A){return false; +}var r=A[p],t=1;if(o){var z=s.nodeName;do{if(r.nodeName!=z){continue;}this[c][this.getUID(r)]=t++;}while((r=r[n]));}else{do{if(r.nodeType!=1){continue; +}this[c][this.getUID(r)]=t++;}while((r=r[n]));}}q=q||"n";var v=this.cacheNTH[q]||this.parseNTHArgument(q);if(!v){return false;}var y=v.a,x=v.b,w=this[c][u]; +if(y==0){return x==w;}if(y>0){if(w":function(p,c,r,o,n,q){if((p=p.firstChild)){do{if(p.nodeType==1){this.push(p,c,r,o,n,q); +}}while((p=p.nextSibling));}},"+":function(p,c,r,o,n,q){while((p=p.nextSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);break;}}},"^":function(p,c,r,o,n,q){p=p.firstChild; +if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:+"](p,c,r,o,n,q);}}},"~":function(q,c,s,p,n,r){while((q=q.nextSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}},"++":function(p,c,r,o,n,q){this["combinator:+"](p,c,r,o,n,q); +this["combinator:!+"](p,c,r,o,n,q);},"~~":function(p,c,r,o,n,q){this["combinator:~"](p,c,r,o,n,q);this["combinator:!~"](p,c,r,o,n,q);},"!":function(p,c,r,o,n,q){while((p=p.parentNode)){if(p!==this.document){this.push(p,c,r,o,n,q); +}}},"!>":function(p,c,r,o,n,q){p=p.parentNode;if(p!==this.document){this.push(p,c,r,o,n,q);}},"!+":function(p,c,r,o,n,q){while((p=p.previousSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q); +break;}}},"!^":function(p,c,r,o,n,q){p=p.lastChild;if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:!+"](p,c,r,o,n,q);}}},"!~":function(q,c,s,p,n,r){while((q=q.previousSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}}};for(var i in j){k["combinator:"+i]=j[i];}var l={empty:function(c){var n=c.firstChild; +return !(n&&n.nodeType==1)&&!(c.innerText||c.textContent||"").length;},not:function(c,n){return !this.matchNode(c,n);},contains:function(c,n){return(c.innerText||c.textContent||"").indexOf(n)>-1; +},"first-child":function(c){while((c=c.previousSibling)){if(c.nodeType==1){return false;}}return true;},"last-child":function(c){while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"only-child":function(o){var n=o;while((n=n.previousSibling)){if(n.nodeType==1){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"nth-child":k.createNTHPseudo("firstChild","nextSibling","posNTH"),"nth-last-child":k.createNTHPseudo("lastChild","previousSibling","posNTHLast"),"nth-of-type":k.createNTHPseudo("firstChild","nextSibling","posNTHType",true),"nth-last-of-type":k.createNTHPseudo("lastChild","previousSibling","posNTHTypeLast",true),index:function(n,c){return this["pseudo:nth-child"](n,""+(c+1)); +},even:function(c){return this["pseudo:nth-child"](c,"2n");},odd:function(c){return this["pseudo:nth-child"](c,"2n+1");},"first-of-type":function(c){var n=c.nodeName; +while((c=c.previousSibling)){if(c.nodeName==n){return false;}}return true;},"last-of-type":function(c){var n=c.nodeName;while((c=c.nextSibling)){if(c.nodeName==n){return false; +}}return true;},"only-of-type":function(o){var n=o,p=o.nodeName;while((n=n.previousSibling)){if(n.nodeName==p){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeName==p){return false; +}}return true;},enabled:function(c){return !c.disabled;},disabled:function(c){return c.disabled;},checked:function(c){return c.checked||c.selected;},focus:function(c){return this.isHTMLDocument&&this.document.activeElement===c&&(c.href||c.type||this.hasAttribute(c,"tabindex")); +},root:function(c){return(c===this.root);},selected:function(c){return c.selected;}};for(var b in l){k["pseudo:"+b]=l[b];}var a=k.attributeGetters={"for":function(){return("htmlFor" in this)?this.htmlFor:this.getAttribute("for"); +},href:function(){return("href" in this)?this.getAttribute("href",2):this.getAttribute("href");},style:function(){return(this.style)?this.style.cssText:this.getAttribute("style"); +},tabindex:function(){var c=this.getAttributeNode("tabindex");return(c&&c.specified)?c.nodeValue:null;},type:function(){return this.getAttribute("type"); +},maxlength:function(){var c=this.getAttributeNode("maxLength");return(c&&c.specified)?c.nodeValue:null;}};a.MAXLENGTH=a.maxLength=a.maxlength;var e=k.Slick=(this.Slick||{}); +e.version="1.1.7";e.search=function(n,o,c){return k.search(n,o,c);};e.find=function(c,n){return k.search(c,n,null,true);};e.contains=function(c,n){k.setDocument(c); +return k.contains(c,n);};e.getAttribute=function(n,c){k.setDocument(n);return k.getAttribute(n,c);};e.hasAttribute=function(n,c){k.setDocument(n);return k.hasAttribute(n,c); +};e.match=function(n,c){if(!(n&&c)){return false;}if(!c||c===n){return true;}k.setDocument(n);return k.matchNode(n,c);};e.defineAttributeGetter=function(c,n){k.attributeGetters[c]=n; +return this;};e.lookupAttributeGetter=function(c){return k.attributeGetters[c];};e.definePseudo=function(c,n){k["pseudo:"+c]=function(p,o){return n.call(p,o); +};return this;};e.lookupPseudo=function(c){var n=k["pseudo:"+c];if(n){return function(o){return n.call(this,o);};}return null;};e.override=function(n,c){k.override(n,c); +return this;};e.isXML=k.isXML;e.uidOf=function(c){return k.getUIDHTML(c);};if(!this.Slick){this.Slick=e;}}).apply((typeof exports!="undefined")?exports:this); +var Element=function(b,g){var h=Element.Constructors[b];if(h){return h(g);}if(typeof b!="string"){return document.id(b).set(g);}if(!g){g={};}if(!(/^[\w-]+$/).test(b)){var e=Slick.parse(b).expressions[0][0]; +b=(e.tag=="*")?"div":e.tag;if(e.id&&g.id==null){g.id=e.id;}var d=e.attributes;if(d){for(var a,f=0,c=d.length;f=this.length){delete this[g--];}return e;}.protect());}Array.forEachMethod(function(g,e){Elements.implement(e,g);});Array.mirror(Elements);var d; +try{d=(document.createElement("").name=="x");}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked; +}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g); +}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this; +},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null; +},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G); +};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l; +};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document); +});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements); +},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m); +}if(!document.createElement("div").contains){Element.implement(m);}Element.implement("hasChild",function(e){return this!==e&&this.contains(e);});(function(l,E,e){this.Selectors={}; +var F=this.Selectors.Pseudo=new Hash();var D=function(){for(var G in F){if(F.hasOwnProperty(G)){Slick.definePseudo(G,F[G]);delete F[G];}}};Slick.search=function(H,I,G){D(); +return l.call(this,H,I,G);};Slick.find=function(G,H){D();return E.call(this,G,H);};Slick.match=function(H,G){D();return e.call(this,H,G);};})(Slick.search,Slick.find,Slick.match); +var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions;for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e)); +});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e)); +});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast()); +},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1"))); +},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){var H=new Elements;if(arguments.length==1&&typeof e=="string"){return Slick.search(this.document,e,H); +}var E=Array.flatten(arguments);for(var F=0,D=E.length;F(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F); +return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e); +},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D]; +(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l); +}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," "); +},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1"); +return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length; +if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D";var a=(t.childNodes.length==1);if(!a){var s="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),b=document.createDocumentFragment(),u=s.length; +while(u--){b.createElement(s[u]);}}t=null;var g=Function.attempt(function(){var e=document.createElement("table");e.innerHTML="";return true; +});var c=document.createElement("tr"),o="";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"","
    "],select:[1,""],tbody:[2,"","
    "],tr:[3,"","
    "]}; +e.thead=e.tfoot=e.tbody;return function(D){var E=e[this.get("tag")];if(!E&&!a){E=[0,"",""];}if(!E){return l.call(this,D);}var H=E[0],G=document.createElement("div"),F=G; +if(!a){b.appendChild(G);}G.innerHTML=[E[1],D,E[2]].flatten().join("");while(H--){F=F.firstChild;}this.empty().adopt(F.childNodes);if(!a){b.removeChild(G); +}G=null;};})(Element.Properties.html.set);}var n=document.createElement("form");n.innerHTML="";if(n.firstChild.value!="s"){Element.Properties.value={set:function(G){var l=this.get("tag"); +if(l!="select"){return this.setProperty("value",G);}var D=this.getElements("option");for(var E=0;E0||k==null?"visible":"hidden";};var f=(h?function(l,k){l.style.opacity=k;}:(e?function(l,k){var n=l.style; +if(!l.currentStyle||!l.currentStyle.hasLayout){n.zoom=1;}if(k==null||k==1){k="";}else{k="alpha(opacity="+(k*100).limit(0,100).round()+")";}var m=n.filter||l.getComputedStyle("filter")||""; +n.filter=j.test(m)?m.replace(j,k):m+k;if(!n.filter){n.removeAttribute("filter");}}:a));var g=(h?function(l){var k=l.style.opacity||l.getComputedStyle("opacity"); +return(k=="")?1:k.toFloat();}:(e?function(l){var m=(l.style.filter||l.getComputedStyle("filter")),k;if(m){k=m.match(j);}return(k==null||m==null)?1:(k[1]/100); +}:function(l){var k=l.retrieve("$opacity");if(k==null){k=(l.style.visibility=="hidden"?0:1);}return k;}));var b=(i.style.cssFloat==null)?"styleFloat":"cssFloat"; +Element.implement({getComputedStyle:function(m){if(this.currentStyle){return this.currentStyle[m.camelCase()];}var l=Element.getDocument(this).defaultView,k=l?l.getComputedStyle(this,null):null; +return(k)?k.getPropertyValue((m==b)?"float":m.hyphenate()):null;},setStyle:function(l,k){if(l=="opacity"){if(k!=null){k=parseFloat(k);}f(this,k);return this; +}l=(l=="float"?b:l).camelCase();if(typeOf(k)!="string"){var m=(Element.Styles[l]||"@").split(" ");k=Array.from(k).map(function(o,n){if(!m[n]){return""; +}return(typeOf(o)=="number")?m[n].replace("@",Math.round(o)):o;}).join(" ");}else{if(k==String(Number(k))){k=Math.round(k);}}this.style[l]=k;if((k==""||k==null)&&c&&this.style.removeAttribute){this.style.removeAttribute(l); +}return this;},getStyle:function(q){if(q=="opacity"){return g(this);}q=(q=="float"?b:q).camelCase();var k=this.style[q];if(!k||q=="zIndex"){k=[];for(var p in Element.ShortStyles){if(q!=p){continue; +}for(var o in Element.ShortStyles[p]){k.push(this.getStyle(o));}return k.join(" ");}k=this.getComputedStyle(q);}if(k){k=String(k);var m=k.match(/rgba?\([\d\s,]+\)/); +if(m){k=k.replace(m[0],m[0].rgbToHex());}}if(Browser.opera||Browser.ie){if((/^(height|width)$/).test(q)&&!(/px$/.test(k))){var l=(q=="width")?["left","right"]:["top","bottom"],n=0; +l.each(function(r){n+=this.getStyle("border-"+r+"-width").toInt()+this.getStyle("padding-"+r).toInt();},this);return this["offset"+q.capitalize()]-n+"px"; +}if(Browser.ie&&(/^border(.+)Width|margin|padding/).test(q)&&isNaN(parseFloat(k))){return"0px";}}return k;},setStyles:function(l){for(var k in l){this.setStyle(k,l[k]); +}return this;},getStyles:function(){var k={};Array.flatten(arguments).each(function(l){k[l]=this.getStyle(l);},this);return k;}});Element.Styles={left:"@px",top:"@px",bottom:"@px",right:"@px",width:"@px",height:"@px",maxWidth:"@px",maxHeight:"@px",minWidth:"@px",minHeight:"@px",backgroundColor:"rgb(@, @, @)",backgroundPosition:"@px @px",color:"rgb(@, @, @)",fontSize:"@px",letterSpacing:"@px",lineHeight:"@px",clip:"rect(@px @px @px @px)",margin:"@px @px @px @px",padding:"@px @px @px @px",border:"@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)",borderWidth:"@px @px @px @px",borderStyle:"@ @ @ @",borderColor:"rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)",zIndex:"@",zoom:"@",fontWeight:"@",textIndent:"@px",opacity:"@"}; +Element.implement({setOpacity:function(k){f(this,k);return this;},getOpacity:function(){return g(this);}});Element.Properties.opacity={set:function(k){f(this,k); +a(this,k);},get:function(){return g(this);}};Element.Styles=new Hash(Element.Styles);Element.ShortStyles={margin:{},padding:{},border:{},borderWidth:{},borderStyle:{},borderColor:{}}; +["Top","Right","Bottom","Left"].each(function(q){var p=Element.ShortStyles;var l=Element.Styles;["margin","padding"].each(function(r){var s=r+q;p[r][s]=l[s]="@px"; +});var o="border"+q;p.border[o]=l[o]="@px @ rgb(@, @, @)";var n=o+"Width",k=o+"Style",m=o+"Color";p[o]={};p.borderWidth[n]=p[o][n]=l[n]="@px";p.borderStyle[k]=p[o][k]=l[k]="@"; +p.borderColor[m]=p[o][m]=l[m]="rgb(@, @, @)";});})();(function(){Element.Properties.events={set:function(b){this.addEvents(b);}};[Element,Window,Document].invoke("implement",{addEvent:function(f,h){var i=this.retrieve("events",{}); +if(!i[f]){i[f]={keys:[],values:[]};}if(i[f].keys.contains(h)){return this;}i[f].keys.push(h);var g=f,b=Element.Events[f],d=h,j=this;if(b){if(b.onAdd){b.onAdd.call(this,h,f); +}if(b.condition){d=function(k){if(b.condition.call(this,k,f)){return h.call(this,k);}return true;};}if(b.base){g=Function.from(b.base).call(this,f);}}var e=function(){return h.call(j); +};var c=Element.NativeEvents[g];if(c){if(c==2){e=function(k){k=new DOMEvent(k,j.getWindow());if(d.call(j,k)===false){k.stop();}};}this.addListener(g,e,arguments[2]); +}i[f].values.push(e);return this;},removeEvent:function(e,d){var c=this.retrieve("events");if(!c||!c[e]){return this;}var h=c[e];var b=h.keys.indexOf(d); +if(b==-1){return this;}var g=h.values[b];delete h.keys[b];delete h.values[b];var f=Element.Events[e];if(f){if(f.onRemove){f.onRemove.call(this,d,e);}if(f.base){e=Function.from(f.base).call(this,e); +}}return(Element.NativeEvents[e])?this.removeListener(e,g,arguments[2]):this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this; +},removeEvents:function(b){var d;if(typeOf(b)=="object"){for(d in b){this.removeEvent(d,b[d]);}return this;}var c=this.retrieve("events");if(!c){return this; +}if(!b){for(d in c){this.removeEvents(d);}this.eliminate("events");}else{if(c[b]){c[b].keys.each(function(e){this.removeEvent(b,e);},this);delete c[b]; +}}return this;},fireEvent:function(e,c,b){var d=this.retrieve("events");if(!d||!d[e]){return this;}c=Array.from(c);d[e].keys.each(function(f){if(b){f.delay(b,this,c); +}else{f.apply(this,c);}},this);return this;},cloneEvents:function(e,d){e=document.id(e);var c=e.retrieve("events");if(!c){return this;}if(!d){for(var b in c){this.cloneEvents(e,b); +}}else{if(c[d]){c[d].keys.each(function(f){this.addEvent(d,f);},this);}}return this;}});Element.NativeEvents={click:2,dblclick:2,mouseup:2,mousedown:2,contextmenu:2,mousewheel:2,DOMMouseScroll:2,mouseover:2,mouseout:2,mousemove:2,selectstart:2,selectend:2,keydown:2,keypress:2,keyup:2,orientationchange:2,touchstart:2,touchmove:2,touchend:2,touchcancel:2,gesturestart:2,gesturechange:2,gestureend:2,focus:2,blur:2,change:2,reset:2,select:2,submit:2,paste:2,input:2,load:2,unload:1,beforeunload:2,resize:1,move:1,DOMContentLoaded:1,readystatechange:1,error:1,abort:1,scroll:1}; +Element.Events={mousewheel:{base:(Browser.firefox)?"DOMMouseScroll":"mousewheel"}};if("onmouseenter" in document.documentElement){Element.NativeEvents.mouseenter=Element.NativeEvents.mouseleave=2; +}else{var a=function(b){var c=b.relatedTarget;if(c==null){return true;}if(!c){return false;}return(c!=this&&c.prefix!="xul"&&typeOf(this)!="document"&&!this.contains(c)); +};Element.Events.mouseenter={base:"mouseover",condition:a};Element.Events.mouseleave={base:"mouseout",condition:a};}if(!window.addEventListener){Element.NativeEvents.propertychange=2; +Element.Events.change={base:function(){var b=this.type;return(this.get("tag")=="input"&&(b=="radio"||b=="checkbox"))?"propertychange":"change";},condition:function(b){return this.type!="radio"||(b.event.propertyName=="checked"&&this.checked); +}};}Element.Events=new Hash(Element.Events);})();(function(){var c=!!window.addEventListener;Element.NativeEvents.focusin=Element.NativeEvents.focusout=2; +var k=function(l,m,n,o,p){while(p&&p!=l){if(m(p,o)){return n.call(p,o,p);}p=document.id(p.parentNode);}};var a={mouseenter:{base:"mouseover"},mouseleave:{base:"mouseout"},focus:{base:"focus"+(c?"":"in"),capture:true},blur:{base:c?"blur":"focusout",capture:true}}; +var b="$delegation:";var i=function(l){return{base:"focusin",remove:function(m,o){var p=m.retrieve(b+l+"listeners",{})[o];if(p&&p.forms){for(var n=p.forms.length; +n--;){p.forms[n].removeEvent(l,p.fns[n]);}}},listen:function(x,r,v,n,t,s){var o=(t.get("tag")=="form")?t:n.target.getParent("form");if(!o){return;}var u=x.retrieve(b+l+"listeners",{}),p=u[s]||{forms:[],fns:[]},m=p.forms,w=p.fns; +if(m.indexOf(o)!=-1){return;}m.push(o);var q=function(y){k(x,r,v,y,t);};o.addEvent(l,q);w.push(q);u[s]=p;x.store(b+l+"listeners",u);}};};var d=function(l){return{base:"focusin",listen:function(m,n,p,q,r){var o={blur:function(){this.removeEvents(o); +}};o[l]=function(s){k(m,n,p,s,r);};q.target.addEvents(o);}};};if(!c){Object.append(a,{submit:i("submit"),reset:i("reset"),change:d("change"),select:d("select")}); +}var h=Element.prototype,f=h.addEvent,j=h.removeEvent;var e=function(l,m){return function(r,q,n){if(r.indexOf(":relay")==-1){return l.call(this,r,q,n); +}var o=Slick.parse(r).expressions[0][0];if(o.pseudos[0].key!="relay"){return l.call(this,r,q,n);}var p=o.tag;o.pseudos.slice(1).each(function(s){p+=":"+s.key+(s.value?"("+s.value+")":""); +});l.call(this,r,q);return m.call(this,p,o.pseudos[0].value,q);};};var g={addEvent:function(v,q,x){var t=this.retrieve("$delegates",{}),r=t[v];if(r){for(var y in r){if(r[y].fn==x&&r[y].match==q){return this; +}}}var p=v,u=q,o=x,n=a[v]||{};v=n.base||p;q=function(B){return Slick.match(B,u);};var w=Element.Events[p];if(w&&w.condition){var l=q,m=w.condition;q=function(C,B){return l(C,B)&&m.call(C,B,v); +};}var z=this,s=String.uniqueID();var A=n.listen?function(B,C){if(!C&&B&&B.target){C=B.target;}if(C){n.listen(z,q,x,B,C,s);}}:function(B,C){if(!C&&B&&B.target){C=B.target; +}if(C){k(z,q,x,B,C);}};if(!r){r={};}r[s]={match:u,fn:o,delegator:A};t[p]=r;return f.call(this,v,A,n.capture);},removeEvent:function(r,n,t,u){var q=this.retrieve("$delegates",{}),p=q[r]; +if(!p){return this;}if(u){var m=r,w=p[u].delegator,l=a[r]||{};r=l.base||m;if(l.remove){l.remove(this,u);}delete p[u];q[m]=p;return j.call(this,r,w);}var o,v; +if(t){for(o in p){v=p[o];if(v.match==n&&v.fn==t){return g.removeEvent.call(this,r,n,t,o);}}}else{for(o in p){v=p[o];if(v.match==n){g.removeEvent.call(this,r,n,v.fn,o); +}}}return this;}};[Element,Window,Document].invoke("implement",{addEvent:e(f,g.addEvent),removeEvent:e(j,g.removeEvent)});})();(function(){var h=document.createElement("div"),e=document.createElement("div"); +h.style.height="0";h.appendChild(e);var d=(e.offsetParent===h);h=e=null;var l=function(m){return k(m,"position")!="static"||a(m);};var i=function(m){return l(m)||(/^(?:table|td|th)$/i).test(m.tagName); +};Element.implement({scrollTo:function(m,n){if(a(this)){this.getWindow().scrollTo(m,n);}else{this.scrollLeft=m;this.scrollTop=n;}return this;},getSize:function(){if(a(this)){return this.getWindow().getSize(); +}return{x:this.offsetWidth,y:this.offsetHeight};},getScrollSize:function(){if(a(this)){return this.getWindow().getScrollSize();}return{x:this.scrollWidth,y:this.scrollHeight}; +},getScroll:function(){if(a(this)){return this.getWindow().getScroll();}return{x:this.scrollLeft,y:this.scrollTop};},getScrolls:function(){var n=this.parentNode,m={x:0,y:0}; +while(n&&!a(n)){m.x+=n.scrollLeft;m.y+=n.scrollTop;n=n.parentNode;}return m;},getOffsetParent:d?function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}var n=(k(m,"position")=="static")?i:l;while((m=m.parentNode)){if(n(m)){return m;}}return null;}:function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}try{return m.offsetParent;}catch(n){}return null;},getOffsets:function(){if(this.getBoundingClientRect&&!Browser.Platform.ios){var r=this.getBoundingClientRect(),o=document.id(this.getDocument().documentElement),q=o.getScroll(),t=this.getScrolls(),s=(k(this,"position")=="fixed"); +return{x:r.left.toInt()+t.x+((s)?0:q.x)-o.clientLeft,y:r.top.toInt()+t.y+((s)?0:q.y)-o.clientTop};}var n=this,m={x:0,y:0};if(a(this)){return m;}while(n&&!a(n)){m.x+=n.offsetLeft; +m.y+=n.offsetTop;if(Browser.firefox){if(!c(n)){m.x+=b(n);m.y+=g(n);}var p=n.parentNode;if(p&&k(p,"overflow")!="visible"){m.x+=b(p);m.y+=g(p);}}else{if(n!=this&&Browser.safari){m.x+=b(n); +m.y+=g(n);}}n=n.offsetParent;}if(Browser.firefox&&!c(this)){m.x-=b(this);m.y-=g(this);}return m;},getPosition:function(p){var q=this.getOffsets(),n=this.getScrolls(); +var m={x:q.x-n.x,y:q.y-n.y};if(p&&(p=document.id(p))){var o=p.getPosition();return{x:m.x-o.x-b(p),y:m.y-o.y-g(p)};}return m;},getCoordinates:function(o){if(a(this)){return this.getWindow().getCoordinates(); +}var m=this.getPosition(o),n=this.getSize();var p={left:m.x,top:m.y,width:n.x,height:n.y};p.right=p.left+p.width;p.bottom=p.top+p.height;return p;},computePosition:function(m){return{left:m.x-j(this,"margin-left"),top:m.y-j(this,"margin-top")}; +},setPosition:function(m){return this.setStyles(this.computePosition(m));}});[Document,Window].invoke("implement",{getSize:function(){var m=f(this);return{x:m.clientWidth,y:m.clientHeight}; +},getScroll:function(){var n=this.getWindow(),m=f(this);return{x:n.pageXOffset||m.scrollLeft,y:n.pageYOffset||m.scrollTop};},getScrollSize:function(){var o=f(this),n=this.getSize(),m=this.getDocument().body; +return{x:Math.max(o.scrollWidth,m.scrollWidth,n.x),y:Math.max(o.scrollHeight,m.scrollHeight,n.y)};},getPosition:function(){return{x:0,y:0};},getCoordinates:function(){var m=this.getSize(); +return{top:0,left:0,bottom:m.y,right:m.x,height:m.y,width:m.x};}});var k=Element.getComputedStyle;function j(m,n){return k(m,n).toInt()||0;}function c(m){return k(m,"-moz-box-sizing")=="border-box"; +}function g(m){return j(m,"border-top-width");}function b(m){return j(m,"border-left-width");}function a(m){return(/^(?:body|html)$/i).test(m.tagName); +}function f(m){var n=m.getDocument();return(!n.compatMode||n.compatMode=="CSS1Compat")?n.html:n.body;}})();Element.alias({position:"setPosition"});[Window,Document,Element].invoke("implement",{getHeight:function(){return this.getSize().y; +},getWidth:function(){return this.getSize().x;},getScrollTop:function(){return this.getScroll().y;},getScrollLeft:function(){return this.getScroll().x; +},getScrollHeight:function(){return this.getScrollSize().y;},getScrollWidth:function(){return this.getScrollSize().x;},getTop:function(){return this.getPosition().y; +},getLeft:function(){return this.getPosition().x;}});(function(){var f=this.Fx=new Class({Implements:[Chain,Events,Options],options:{fps:60,unit:false,duration:500,frames:null,frameSkip:true,link:"ignore"},initialize:function(g){this.subject=this.subject||this; +this.setOptions(g);},getTransition:function(){return function(g){return -(Math.cos(Math.PI*g)-1)/2;};},step:function(g){if(this.options.frameSkip){var h=(this.time!=null)?(g-this.time):0,i=h/this.frameInterval; +this.time=g;this.frame+=i;}else{this.frame++;}if(this.frame=(7-4*d)/11){e=c*c-Math.pow((11-6*d-11*f)/4,2);break;}}return e;},Elastic:function(b,a){return Math.pow(2,10*--b)*Math.cos(20*b*Math.PI*(a&&a[0]||1)/3); +}});["Quad","Cubic","Quart","Quint"].each(function(b,a){Fx.Transitions[b]=new Fx.Transition(function(c){return Math.pow(c,a+2);});});(function(){var d=function(){},a=("onprogress" in new Browser.Request); +var c=this.Request=new Class({Implements:[Chain,Events,Options],options:{url:"",data:"",headers:{"X-Requested-With":"XMLHttpRequest",Accept:"text/javascript, text/html, application/xml, text/xml, */*"},async:true,format:false,method:"post",link:"ignore",isSuccess:null,emulation:true,urlEncoded:true,encoding:"utf-8",evalScripts:false,evalResponse:false,timeout:0,noCache:false},initialize:function(e){this.xhr=new Browser.Request(); +this.setOptions(e);this.headers=this.options.headers;},onStateChange:function(){var e=this.xhr;if(e.readyState!=4||!this.running){return;}this.running=false; +this.status=0;Function.attempt(function(){var f=e.status;this.status=(f==1223)?204:f;}.bind(this));e.onreadystatechange=d;if(a){e.onprogress=e.onloadstart=d; +}clearTimeout(this.timer);this.response={text:this.xhr.responseText||"",xml:this.xhr.responseXML};if(this.options.isSuccess.call(this,this.status)){this.success(this.response.text,this.response.xml); +}else{this.failure();}},isSuccess:function(){var e=this.status;return(e>=200&&e<300);},isRunning:function(){return !!this.running;},processScripts:function(e){if(this.options.evalResponse||(/(ecma|java)script/).test(this.getHeader("Content-type"))){return Browser.exec(e); +}return e.stripScripts(this.options.evalScripts);},success:function(f,e){this.onSuccess(this.processScripts(f),e);},onSuccess:function(){this.fireEvent("complete",arguments).fireEvent("success",arguments).callChain(); +},failure:function(){this.onFailure();},onFailure:function(){this.fireEvent("complete").fireEvent("failure",this.xhr);},loadstart:function(e){this.fireEvent("loadstart",[e,this.xhr]); +},progress:function(e){this.fireEvent("progress",[e,this.xhr]);},timeout:function(){this.fireEvent("timeout",this.xhr);},setHeader:function(e,f){this.headers[e]=f; +return this;},getHeader:function(e){return Function.attempt(function(){return this.xhr.getResponseHeader(e);}.bind(this));},check:function(){if(!this.running){return true; +}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));return false;}return false;},send:function(o){if(!this.check(o)){return this; +}this.options.isSuccess=this.options.isSuccess||this.isSuccess;this.running=true;var l=typeOf(o);if(l=="string"||l=="element"){o={data:o};}var h=this.options; +o=Object.append({data:h.data,url:h.url,method:h.method},o);var j=o.data,f=String(o.url),e=o.method.toLowerCase();switch(typeOf(j)){case"element":j=document.id(j).toQueryString(); +break;case"object":case"hash":j=Object.toQueryString(j);}if(this.options.format){var m="format="+this.options.format;j=(j)?m+"&"+j:m;}if(this.options.emulation&&!["get","post"].contains(e)){var k="_method="+e; +j=(j)?k+"&"+j:k;e="post";}if(this.options.urlEncoded&&["post","put"].contains(e)){var g=(this.options.encoding)?"; charset="+this.options.encoding:"";this.headers["Content-type"]="application/x-www-form-urlencoded"+g; +}if(!f){f=document.location.pathname;}var i=f.lastIndexOf("/");if(i>-1&&(i=f.indexOf("#"))>-1){f=f.substr(0,i);}if(this.options.noCache){f+=(f.contains("?")?"&":"?")+String.uniqueID(); +}if(j&&e=="get"){f+=(f.contains("?")?"&":"?")+j;j=null;}var n=this.xhr;if(a){n.onloadstart=this.loadstart.bind(this);n.onprogress=this.progress.bind(this); +}n.open(e.toUpperCase(),f,this.options.async,this.options.user,this.options.password);if(this.options.user&&"withCredentials" in n){n.withCredentials=true; +}n.onreadystatechange=this.onStateChange.bind(this);Object.each(this.headers,function(q,p){try{n.setRequestHeader(p,q);}catch(r){this.fireEvent("exception",[p,q]); +}},this);this.fireEvent("request");n.send(j);if(!this.options.async){this.onStateChange();}else{if(this.options.timeout){this.timer=this.timeout.delay(this.options.timeout,this); +}}return this;},cancel:function(){if(!this.running){return this;}this.running=false;var e=this.xhr;e.abort();clearTimeout(this.timer);e.onreadystatechange=d; +if(a){e.onprogress=e.onloadstart=d;}this.xhr=new Browser.Request();this.fireEvent("cancel");return this;}});var b={};["get","post","put","delete","GET","POST","PUT","DELETE"].each(function(e){b[e]=function(g){var f={method:e}; +if(g!=null){f.data=g;}return this.send(f);};});c.implement(b);Element.Properties.send={set:function(e){var f=this.get("send").cancel();f.setOptions(e); +return this;},get:function(){var e=this.retrieve("send");if(!e){e=new c({data:this,link:"cancel",method:this.get("method")||"post",url:this.get("action")}); +this.store("send",e);}return e;}};Element.implement({send:function(e){var f=this.get("send");f.send({data:this,url:e||f.options.url});return this;}});})(); +Request.HTML=new Class({Extends:Request,options:{update:false,append:false,evalScripts:true,filter:false,headers:{Accept:"text/html, application/xml, text/xml, */*"}},success:function(f){var e=this.options,c=this.response; +c.html=f.stripScripts(function(h){c.javascript=h;});var d=c.html.match(/]*>([\s\S]*?)<\/body>/i);if(d){c.html=d[1];}var b=new Element("div").set("html",c.html); +c.tree=b.childNodes;c.elements=b.getElements(e.filter||"*");if(e.filter){c.tree=c.elements;}if(e.update){var g=document.id(e.update).empty();if(e.filter){g.adopt(c.elements); +}else{g.set("html",c.html);}}else{if(e.append){var a=document.id(e.append);if(e.filter){c.elements.reverse().inject(a);}else{a.adopt(b.getChildren());}}}if(e.evalScripts){Browser.exec(c.javascript); +}this.onSuccess(c.tree,c.elements,c.html,c.javascript);}});Element.Properties.load={set:function(a){var b=this.get("load").cancel();b.setOptions(a);return this; +},get:function(){var a=this.retrieve("load");if(!a){a=new Request.HTML({data:this,link:"cancel",update:this,method:"get"});this.store("load",a);}return a; +}};Element.implement({load:function(){this.get("load").send(Array.link(arguments,{data:Type.isObject,url:Type.isString}));return this;}});if(typeof JSON=="undefined"){this.JSON={}; +}JSON=new Hash({stringify:JSON.stringify,parse:JSON.parse});(function(){var special={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"}; +var escape=function(chr){return special[chr]||"\\u"+("0000"+chr.charCodeAt(0).toString(16)).slice(-4);};JSON.validate=function(string){string=string.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""); +return(/^[\],:{}\s]*$/).test(string);};JSON.encode=JSON.stringify?function(obj){return JSON.stringify(obj);}:function(obj){if(obj&&obj.toJSON){obj=obj.toJSON(); +}switch(typeOf(obj)){case"string":return'"'+obj.replace(/[\x00-\x1f\\"]/g,escape)+'"';case"array":return"["+obj.map(JSON.encode).clean()+"]";case"object":case"hash":var string=[]; +Object.each(obj,function(value,key){var json=JSON.encode(value);if(json){string.push(JSON.encode(key)+":"+json);}});return"{"+string+"}";case"number":case"boolean":return""+obj; +case"null":return"null";}return null;};JSON.decode=function(string,secure){if(!string||typeOf(string)!="string"){return null;}if(secure||JSON.secure){if(JSON.parse){return JSON.parse(string); +}if(!JSON.validate(string)){throw new Error("JSON could not decode the input; security is enabled and the value is not secure.");}}return eval("("+string+")"); +};})();Request.JSON=new Class({Extends:Request,options:{secure:true},initialize:function(a){this.parent(a);Object.append(this.headers,{Accept:"application/json","X-Request":"JSON"}); +},success:function(c){var b;try{b=this.response.json=JSON.decode(c,this.options.secure);}catch(a){this.fireEvent("error",[c,a]);return;}if(b==null){this.onFailure(); +}else{this.onSuccess(b,c);}}});var Cookie=new Class({Implements:Options,options:{path:"/",domain:false,duration:false,secure:false,document:document,encode:true},initialize:function(b,a){this.key=b; +this.setOptions(a);},write:function(b){if(this.options.encode){b=encodeURIComponent(b);}if(this.options.domain){b+="; domain="+this.options.domain;}if(this.options.path){b+="; path="+this.options.path; +}if(this.options.duration){var a=new Date();a.setTime(a.getTime()+this.options.duration*24*60*60*1000);b+="; expires="+a.toGMTString();}if(this.options.secure){b+="; secure"; +}this.options.document.cookie=this.key+"="+b;return this;},read:function(){var a=this.options.document.cookie.match("(?:^|;)\\s*"+this.key.escapeRegExp()+"=([^;]*)"); +return(a)?decodeURIComponent(a[1]):null;},dispose:function(){new Cookie(this.key,Object.merge({},this.options,{duration:-1})).write("");return this;}}); +Cookie.write=function(b,c,a){return new Cookie(b,a).write(c);};Cookie.read=function(a){return new Cookie(a).read();};Cookie.dispose=function(b,a){return new Cookie(b,a).dispose(); +};(function(i,k){var l,f,e=[],c,b,d=k.createElement("div");var g=function(){clearTimeout(b);if(l){return;}Browser.loaded=l=true;k.removeListener("DOMContentLoaded",g).removeListener("readystatechange",a); +k.fireEvent("domready");i.fireEvent("domready");};var a=function(){for(var m=e.length;m--;){if(e[m]()){g();return true;}}return false;};var j=function(){clearTimeout(b); +if(!a()){b=setTimeout(j,10);}};k.addListener("DOMContentLoaded",g);var h=function(){try{d.doScroll();return true;}catch(m){}return false;};if(d.doScroll&&!h()){e.push(h); +c=true;}if(k.readyState){e.push(function(){var m=k.readyState;return(m=="loaded"||m=="complete");});}if("onreadystatechange" in k){k.addListener("readystatechange",a); +}else{c=true;}if(c){j();}Element.Events.domready={onAdd:function(m){if(l){m.call(this);}}};Element.Events.load={base:"load",onAdd:function(m){if(f&&this==i){m.call(this); +}},condition:function(){if(this==i){g();delete Element.Events.load;}return true;}};i.addEvent("load",function(){f=true;});})(window,document);(function(){var Swiff=this.Swiff=new Class({Implements:Options,options:{id:null,height:1,width:1,container:null,properties:{},params:{quality:"high",allowScriptAccess:"always",wMode:"window",swLiveConnect:true},callBacks:{},vars:{}},toElement:function(){return this.object; +},initialize:function(path,options){this.instance="Swiff_"+String.uniqueID();this.setOptions(options);options=this.options;var id=this.id=options.id||this.instance; +var container=document.id(options.container);Swiff.CallBacks[this.instance]={};var params=options.params,vars=options.vars,callBacks=options.callBacks; +var properties=Object.append({height:options.height,width:options.width},options.properties);var self=this;for(var callBack in callBacks){Swiff.CallBacks[this.instance][callBack]=(function(option){return function(){return option.apply(self.object,arguments); +};})(callBacks[callBack]);vars[callBack]="Swiff.CallBacks."+this.instance+"."+callBack;}params.flashVars=Object.toQueryString(vars);if(Browser.ie){properties.classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; +params.movie=path;}else{properties.type="application/x-shockwave-flash";}properties.data=path;var build='';}}build+="";this.object=((container)?container.empty():new Element("div")).set("html",build).firstChild; +},replaces:function(element){element=document.id(element,true);element.parentNode.replaceChild(this.toElement(),element);return this;},inject:function(element){document.id(element,true).appendChild(this.toElement()); +return this;},remote:function(){return Swiff.remote.apply(Swiff,[this.toElement()].append(arguments));}});Swiff.CallBacks={};Swiff.remote=function(obj,fn){var rs=obj.CallFunction(''+__flash__argumentsToXML(arguments,2)+""); +return eval(rs);};})(); \ No newline at end of file diff --git a/templates/default/js/mootools-more.js b/templates/default/js/mootools-more.js new file mode 100755 index 0000000..ac0abd2 --- /dev/null +++ b/templates/default/js/mootools-more.js @@ -0,0 +1,472 @@ +// MooTools: the javascript framework. +// Load this file's selection again by visiting: http://mootools.net/more/f137303781e12a3fb874125002b23b78 +// Or build this file again with packager using: packager build More/More More/Class.Refactor More/Chain.Wait More/Array.Extras More/Date More/Date.Extras More/Number.Format More/Object.Extras More/String.Extras More/String.QueryString More/URI More/Hash More/Hash.Extras More/Element.Forms More/Element.Measure More/Element.Pin More/Element.Position More/Element.Shortcuts More/Form.Request More/OverText More/Fx.Elements More/Fx.Accordion More/Fx.Move More/Fx.Reveal More/Fx.Scroll More/Fx.Slide More/Fx.SmoothScroll More/Fx.Sort More/Drag More/Drag.Move More/Slider More/Sortables More/Assets More/Color More/Group More/Mask More/Spinner More/Locale More/Locale.en-US.Date More/Locale.en-US.Number More/Locale.en-GB.Date +/* +--- +copyrights: + - [MooTools](http://mootools.net) + +licenses: + - [MIT License](http://mootools.net/license.txt) +... +*/ +MooTools.More={version:"1.4.0.1",build:"a4244edf2aa97ac8a196fc96082dd35af1abab87"};Class.refactor=function(b,a){Object.each(a,function(e,d){var c=b.prototype[d]; +c=(c&&c.$origin)||c||function(){};b.implement(d,(typeof e=="function")?function(){var f=this.previous;this.previous=c;var g=e.apply(this,arguments);this.previous=f; +return g;}:e);});return b;};(function(){var a={wait:function(b){return this.chain(function(){this.callChain.delay(b==null?500:b,this);return this;}.bind(this)); +}};Chain.implement(a);if(this.Fx){Fx.implement(a);}if(this.Element&&Element.implement&&this.Fx){Element.implement({chains:function(b){Array.from(b||["tween","morph","reveal"]).each(function(c){c=this.get(c); +if(!c){return;}c.setOptions({link:"chain"});},this);return this;},pauseFx:function(c,b){this.chains(b).get(b||"tween").wait(c);return this;}});}})();(function(a){Array.implement({min:function(){return Math.min.apply(null,this); +},max:function(){return Math.max.apply(null,this);},average:function(){return this.length?this.sum()/this.length:0;},sum:function(){var b=0,c=this.length; +if(c){while(c--){b+=this[c];}}return b;},unique:function(){return[].combine(this);},shuffle:function(){for(var c=this.length;c&&--c;){var b=this[c],d=Math.floor(Math.random()*(c+1)); +this[c]=this[d];this[d]=b;}return this;},reduce:function(d,e){for(var c=0,b=this.length;c3&&a<21)?"th":["th","st","nd","rd","th"][Math.min(a%10,4)]; +},lessThanMinuteAgo:"less than a minute ago",minuteAgo:"about a minute ago",minutesAgo:"{delta} minutes ago",hourAgo:"about an hour ago",hoursAgo:"about {delta} hours ago",dayAgo:"1 day ago",daysAgo:"{delta} days ago",weekAgo:"1 week ago",weeksAgo:"{delta} weeks ago",monthAgo:"1 month ago",monthsAgo:"{delta} months ago",yearAgo:"1 year ago",yearsAgo:"{delta} years ago",lessThanMinuteUntil:"less than a minute from now",minuteUntil:"about a minute from now",minutesUntil:"{delta} minutes from now",hourUntil:"about an hour from now",hoursUntil:"about {delta} hours from now",dayUntil:"1 day from now",daysUntil:"{delta} days from now",weekUntil:"1 week from now",weeksUntil:"{delta} weeks from now",monthUntil:"1 month from now",monthsUntil:"{delta} months from now",yearUntil:"1 year from now",yearsUntil:"{delta} years from now"}); +(function(){var a=this.Date;var f=a.Methods={ms:"Milliseconds",year:"FullYear",min:"Minutes",mo:"Month",sec:"Seconds",hr:"Hours"};["Date","Day","FullYear","Hours","Milliseconds","Minutes","Month","Seconds","Time","TimezoneOffset","Week","Timezone","GMTOffset","DayOfYear","LastMonth","LastDayOfMonth","UTCDate","UTCDay","UTCFullYear","AMPM","Ordinal","UTCHours","UTCMilliseconds","UTCMinutes","UTCMonth","UTCSeconds","UTCMilliseconds"].each(function(s){a.Methods[s.toLowerCase()]=s; +});var p=function(u,t,s){if(t==1){return u;}return u28){return 1;}if(y==0&&s<-2){x=new a(x).decrement("day",u); +u=0;}w=new a(x.get("year"),0,1).get("day")||7;if(w>4){t=-7;}}else{w=new a(x.get("year"),0,1).get("day");}t+=x.get("dayofyear");t+=6-u;t+=(7+w-v)%7;return(t/7); +},getOrdinal:function(s){return a.getMsg("ordinal",s||this.get("date"));},getTimezone:function(){return this.toString().replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/,"$1").replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/,"$1$2$3"); +},getGMTOffset:function(){var s=this.get("timezoneOffset");return((s>0)?"-":"+")+p((s.abs()/60).floor(),2)+p(s%60,2);},setAMPM:function(s){s=s.toUpperCase(); +var t=this.get("hr");if(t>11&&s=="AM"){return this.decrement("hour",12);}else{if(t<12&&s=="PM"){return this.increment("hour",12);}}return this;},getAMPM:function(){return(this.get("hr")<12)?"AM":"PM"; +},parse:function(s){this.set("time",a.parse(s));return this;},isValid:function(s){if(!s){s=this;}return typeOf(s)=="date"&&!isNaN(s.valueOf());},format:function(s){if(!this.isValid()){return"invalid date"; +}if(!s){s="%x %X";}if(typeof s=="string"){s=g[s.toLowerCase()]||s;}if(typeof s=="function"){return s(this);}var t=this;return s.replace(/%([a-z%])/gi,function(v,u){switch(u){case"a":return a.getMsg("days_abbr")[t.get("day")]; +case"A":return a.getMsg("days")[t.get("day")];case"b":return a.getMsg("months_abbr")[t.get("month")];case"B":return a.getMsg("months")[t.get("month")]; +case"c":return t.format("%a %b %d %H:%M:%S %Y");case"d":return p(t.get("date"),2);case"e":return p(t.get("date"),2," ");case"H":return p(t.get("hr"),2); +case"I":return p((t.get("hr")%12)||12,2);case"j":return p(t.get("dayofyear"),3);case"k":return p(t.get("hr"),2," ");case"l":return p((t.get("hr")%12)||12,2," "); +case"L":return p(t.get("ms"),3);case"m":return p((t.get("mo")+1),2);case"M":return p(t.get("min"),2);case"o":return t.get("ordinal");case"p":return a.getMsg(t.get("ampm")); +case"s":return Math.round(t/1000);case"S":return p(t.get("seconds"),2);case"T":return t.format("%H:%M:%S");case"U":return p(t.get("week"),2);case"w":return t.get("day"); +case"x":return t.format(a.getMsg("shortDate"));case"X":return t.format(a.getMsg("shortTime"));case"y":return t.get("year").toString().substr(2);case"Y":return t.get("year"); +case"z":return t.get("GMTOffset");case"Z":return t.get("Timezone");}return u;});},toISOString:function(){return this.format("iso8601");}}).alias({toJSON:"toISOString",compare:"diff",strftime:"format"}); +var k=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];var g={db:"%Y-%m-%d %H:%M:%S",compact:"%Y%m%dT%H%M%S","short":"%d %b %H:%M","long":"%B %d, %Y %H:%M",rfc822:function(s){return k[s.get("day")]+s.format(", %d ")+h[s.get("month")]+s.format(" %Y %H:%M:%S %Z"); +},rfc2822:function(s){return k[s.get("day")]+s.format(", %d ")+h[s.get("month")]+s.format(" %Y %H:%M:%S %z");},iso8601:function(s){return(s.getUTCFullYear()+"-"+p(s.getUTCMonth()+1,2)+"-"+p(s.getUTCDate(),2)+"T"+p(s.getUTCHours(),2)+":"+p(s.getUTCMinutes(),2)+":"+p(s.getUTCSeconds(),2)+"."+p(s.getUTCMilliseconds(),3)+"Z"); +}};var c=[],n=a.parse;var r=function(v,x,u){var t=-1,w=a.getMsg(v+"s");switch(typeOf(x)){case"object":t=w[x.get(v)];break;case"number":t=w[x];if(!t){throw new Error("Invalid "+v+" index: "+x); +}break;case"string":var s=w.filter(function(y){return this.test(y);},new RegExp("^"+x,"i"));if(!s.length){throw new Error("Invalid "+v+" string");}if(s.length>1){throw new Error("Ambiguous "+v); +}t=s[0];}return(u)?w.indexOf(t):t;};var i=1900,o=70;a.extend({getMsg:function(t,s){return Locale.get("Date."+t,s);},units:{ms:Function.from(1),second:Function.from(1000),minute:Function.from(60000),hour:Function.from(3600000),day:Function.from(86400000),week:Function.from(608400000),month:function(t,s){var u=new a; +return a.daysInMonth(t!=null?t:u.get("mo"),s!=null?s:u.get("year"))*86400000;},year:function(s){s=s||new a().get("year");return a.isLeapYear(s)?31622400000:31536000000; +}},daysInMonth:function(t,s){return[31,a.isLeapYear(s)?29:28,31,30,31,30,31,31,30,31,30,31][t];},isLeapYear:function(s){return((s%4===0)&&(s%100!==0))||(s%400===0); +},parse:function(v){var u=typeOf(v);if(u=="number"){return new a(v);}if(u!="string"){return v;}v=v.clean();if(!v.length){return null;}var s;c.some(function(w){var t=w.re.exec(v); +return(t)?(s=w.handler(t)):false;});if(!(s&&s.isValid())){s=new a(n(v));if(!(s&&s.isValid())){s=new a(v.toInt());}}return s;},parseDay:function(s,t){return r("day",s,t); +},parseMonth:function(t,s){return r("month",t,s);},parseUTC:function(t){var s=new a(t);var u=a.UTC(s.get("year"),s.get("mo"),s.get("date"),s.get("hr"),s.get("min"),s.get("sec"),s.get("ms")); +return new a(u);},orderIndex:function(s){return a.getMsg("dateOrder").indexOf(s)+1;},defineFormat:function(s,t){g[s]=t;return this;},parsePatterns:c,defineParser:function(s){c.push((s.re&&s.handler)?s:l(s)); +return this;},defineParsers:function(){Array.flatten(arguments).each(a.defineParser);return this;},define2DigitYearStart:function(s){o=s%100;i=s-o;return this; +}}).extend({defineFormats:a.defineFormat.overloadSetter()});var d=function(s){return new RegExp("(?:"+a.getMsg(s).map(function(t){return t.substr(0,3); +}).join("|")+")[a-z]*");};var m=function(s){switch(s){case"T":return"%H:%M:%S";case"x":return((a.orderIndex("month")==1)?"%m[-./]%d":"%d[-./]%m")+"([-./]%y)?"; +case"X":return"%H([.:]%M)?([.:]%S([.:]%s)?)? ?%p? ?%z?";}return null;};var j={d:/[0-2]?[0-9]|3[01]/,H:/[01]?[0-9]|2[0-3]/,I:/0?[1-9]|1[0-2]/,M:/[0-5]?\d/,s:/\d+/,o:/[a-z]*/,p:/[ap]\.?m\.?/,y:/\d{2}|\d{4}/,Y:/\d{4}/,z:/Z|[+-]\d{2}(?::?\d{2})?/}; +j.m=j.I;j.S=j.M;var e;var b=function(s){e=s;j.a=j.A=d("days");j.b=j.B=d("months");c.each(function(u,t){if(u.format){c[t]=l(u.format);}});};var l=function(u){if(!e){return{format:u}; +}var s=[];var t=(u.source||u).replace(/%([a-z])/gi,function(w,v){return m(v)||w;}).replace(/\((?!\?)/g,"(?:").replace(/ (?!\?|\*)/g,",? ").replace(/%([a-z%])/gi,function(w,v){var x=j[v]; +if(!x){return v;}s.push(v);return"("+x.source+")";}).replace(/\[a-z\]/gi,"[a-z\\u00c0-\\uffff;&]");return{format:u,re:new RegExp("^"+t+"$","i"),handler:function(y){y=y.slice(1).associate(s); +var v=new a().clearTime(),x=y.y||y.Y;if(x!=null){q.call(v,"y",x);}if("d" in y){q.call(v,"d",1);}if("m" in y||y.b||y.B){q.call(v,"m",1);}for(var w in y){q.call(v,w,y[w]); +}return v;}};};var q=function(s,t){if(!t){return this;}switch(s){case"a":case"A":return this.set("day",a.parseDay(t,true));case"b":case"B":return this.set("mo",a.parseMonth(t,true)); +case"d":return this.set("date",t);case"H":case"I":return this.set("hr",t);case"m":return this.set("mo",t-1);case"M":return this.set("min",t);case"p":return this.set("ampm",t.replace(/\./g,"")); +case"S":return this.set("sec",t);case"s":return this.set("ms",("0."+t)*1000);case"w":return this.set("day",t);case"Y":return this.set("year",t);case"y":t=+t; +if(t<100){t+=i+(t0.75*a){e=c;}break;}f/=a;e=c+"s";}f=f.round();return Date.getMsg(e+d,f).substitute({delta:f});}}).defineParsers({re:/^(?:tod|tom|yes)/i,handler:function(a){var b=new Date().clearTime(); +switch(a[0]){case"tom":return b.increment();case"yes":return b.decrement();default:return b;}}},{re:/^(next|last) ([a-z]+)$/i,handler:function(e){var f=new Date().clearTime(); +var b=f.getDay();var c=Date.parseDay(e[2],true);var a=c-b;if(c<=b){a+=7;}if(e[1]=="last"){a-=7;}return f.set("date",f.getDate()+a);}}).alias("timeAgoInWords","timeDiffInWords"); +Locale.define("en-US","Number",{decimal:".",group:",",currency:{prefix:"$ "}});Number.implement({format:function(q){var n=this;q=q?Object.clone(q):{};var a=function(i){if(q[i]!=null){return q[i]; +}return Locale.get("Number."+i);};var f=n<0,h=a("decimal"),k=a("precision"),o=a("group"),c=a("decimals");if(f){var e=a("negative")||{};if(e.prefix==null&&e.suffix==null){e.prefix="-"; +}["prefix","suffix"].each(function(i){if(e[i]){q[i]=a(i)+e[i];}});n=-n;}var l=a("prefix"),p=a("suffix");if(c!==""&&c>=0&&c<=20){n=n.toFixed(c);}if(k>=1&&k<=21){n=(+n).toPrecision(k); +}n+="";var m;if(a("scientific")===false&&n.indexOf("e")>-1){var j=n.split("e"),b=+j[1];n=j[0].replace(".","");if(b<0){b=-b-1;m=j[0].indexOf(".");if(m>-1){b-=m-1; +}while(b--){n="0"+n;}n="0."+n;}else{m=j[0].lastIndexOf(".");if(m>-1){b-=j[0].length-m-1;}while(b--){n+="0";}}}if(h!="."){n=n.replace(".",h);}if(o){m=n.lastIndexOf(h); +m=(m>-1)?m:n.length;var d=n.substring(m),g=m;while(g--){if((m-g-1)%3==0&&g!=(m-1)){d=o+d;}d=n.charAt(g)+d;}n=d;}if(l){n=l+n;}if(p){n+=p;}return n;},formatCurrency:function(b){var a=Locale.get("Number.currency")||{}; +if(a.scientific==null){a.scientific=false;}a.decimals=b!=null?b:(a.decimals==null?2:a.decimals);return this.format(a);},formatPercentage:function(b){var a=Locale.get("Number.percentage")||{}; +if(a.suffix==null){a.suffix="%";}a.decimals=b!=null?b:(a.decimals==null?2:a.decimals);return this.format(a);}});(function(){var c={a:/[àáâãäåăą]/g,A:/[ÀÁÂÃÄÅĂĄ]/g,c:/[ćčç]/g,C:/[ĆČÇ]/g,d:/[ďđ]/g,D:/[ĎÐ]/g,e:/[èéêëěę]/g,E:/[ÈÉÊËĚĘ]/g,g:/[ğ]/g,G:/[Ğ]/g,i:/[ìíîï]/g,I:/[ÌÍÎÏ]/g,l:/[ĺľł]/g,L:/[ĹĽŁ]/g,n:/[ñňń]/g,N:/[ÑŇŃ]/g,o:/[òóôõöøő]/g,O:/[ÒÓÔÕÖØ]/g,r:/[řŕ]/g,R:/[ŘŔ]/g,s:/[ššş]/g,S:/[ŠŞŚ]/g,t:/[ťţ]/g,T:/[ŤŢ]/g,ue:/[ü]/g,UE:/[Ü]/g,u:/[ùúûůµ]/g,U:/[ÙÚÛŮ]/g,y:/[ÿý]/g,Y:/[ŸÝ]/g,z:/[žźż]/g,Z:/[ŽŹŻ]/g,th:/[þ]/g,TH:/[Þ]/g,dh:/[ð]/g,DH:/[Ð]/g,ss:/[ß]/g,oe:/[œ]/g,OE:/[Œ]/g,ae:/[æ]/g,AE:/[Æ]/g},b={" ":/[\xa0\u2002\u2003\u2009]/g,"*":/[\xb7]/g,"'":/[\u2018\u2019]/g,'"':/[\u201c\u201d]/g,"...":/[\u2026]/g,"-":/[\u2013]/g,"»":/[\uFFFD]/g}; +var a=function(f,h){var e=f,g;for(g in h){e=e.replace(h[g],g);}return e;};var d=function(e,g){e=e||"";var h=g?"<"+e+"(?!\\w)[^>]*>([\\s\\S]*?)":"]+)?>",f=new RegExp(h,"gi"); +return f;};String.implement({standardize:function(){return a(this,c);},repeat:function(e){return new Array(e+1).join(this);},pad:function(e,h,g){if(this.length>=e){return this; +}var f=(h==null?" ":""+h).repeat(e-this.length).substr(0,e-this.length);if(!g||g=="right"){return this+f;}if(g=="left"){return f+this;}return f.substr(0,(f.length/2).floor())+this+f.substr(0,(f.length/2).ceil()); +},getTags:function(e,f){return this.match(d(e,f))||[];},stripTags:function(e,f){return this.replace(d(e,f),"");},tidy:function(){return a(this,b);},truncate:function(e,f,i){var h=this; +if(f==null&&arguments.length==1){f="…";}if(h.length>e){h=h.substring(0,e);if(i){var g=h.lastIndexOf(i);if(g!=-1){h=h.substr(0,g);}}if(f){h+=f;}}return h; +}});})();String.implement({parseQueryString:function(d,a){if(d==null){d=true;}if(a==null){a=true;}var c=this.split(/[&;]/),b={};if(!c.length){return b; +}c.each(function(i){var e=i.indexOf("=")+1,g=e?i.substr(e):"",f=e?i.substr(0,e-1).match(/([^\]\[]+|(\B)(?=\]))/g):[i],h=b;if(!f){return;}if(a){g=decodeURIComponent(g); +}f.each(function(k,j){if(d){k=decodeURIComponent(k);}var l=h[k];if(j0){c.pop(); +}else{if(f!="."){c.push(f);}}});return c.join("/")+"/";},combine:function(c){return c.value||c.scheme+"://"+(c.user?c.user+(c.password?":"+c.password:"")+"@":"")+(c.host||"")+(c.port&&c.port!=this.schemes[c.scheme]?":"+c.port:"")+(c.directory||"/")+(c.file||"")+(c.query?"?"+c.query:"")+(c.fragment?"#"+c.fragment:""); +},set:function(d,f,e){if(d=="value"){var c=f.match(a.regs.scheme);if(c){c=c[1];}if(c&&this.schemes[c.toLowerCase()]==null){this.parsed={scheme:c,value:f}; +}else{this.parsed=this.parse(f,(e||this).parsed)||(c?{scheme:c,value:f}:{value:f});}}else{if(d=="data"){this.setData(f);}else{this.parsed[d]=f;}}return this; +},get:function(c,d){switch(c){case"value":return this.combine(this.parsed,d?d.parsed:false);case"data":return this.getData();}return this.parsed[c]||""; +},go:function(){document.location.href=this.toString();},toURI:function(){return this;},getData:function(e,d){var c=this.get(d||"query");if(!(c||c===0)){return e?null:{}; +}var f=c.parseQueryString();return e?f[e]:f;},setData:function(c,f,d){if(typeof c=="string"){var e=this.getData();e[arguments[0]]=arguments[1];c=e;}else{if(f){c=Object.merge(this.getData(),c); +}}return this.set(d||"query",Object.toQueryString(c));},clearData:function(c){return this.set(c||"query","");},toString:b,valueOf:b});a.regs={endSlash:/\/$/,scheme:/^(\w+):/,directoryDot:/\.\/|\.$/}; +a.base=new a(Array.from(document.getElements("base[href]",true)).getLast(),{base:document.location});String.implement({toURI:function(c){return new a(this,c); +}});})();(function(){if(this.Hash){return;}var a=this.Hash=new Type("Hash",function(b){if(typeOf(b)=="hash"){b=Object.clone(b.getClean());}for(var c in b){this[c]=b[c]; +}return this;});this.$H=function(b){return new a(b);};a.implement({forEach:function(b,c){Object.forEach(this,b,c);},getClean:function(){var c={};for(var b in this){if(this.hasOwnProperty(b)){c[b]=this[b]; +}}return c;},getLength:function(){var c=0;for(var b in this){if(this.hasOwnProperty(b)){c++;}}return c;}});a.alias("each","forEach");a.implement({has:Object.prototype.hasOwnProperty,keyOf:function(b){return Object.keyOf(this,b); +},hasValue:function(b){return Object.contains(this,b);},extend:function(b){a.each(b||{},function(d,c){a.set(this,c,d);},this);return this;},combine:function(b){a.each(b||{},function(d,c){a.include(this,c,d); +},this);return this;},erase:function(b){if(this.hasOwnProperty(b)){delete this[b];}return this;},get:function(b){return(this.hasOwnProperty(b))?this[b]:null; +},set:function(b,c){if(!this[b]||this.hasOwnProperty(b)){this[b]=c;}return this;},empty:function(){a.each(this,function(c,b){delete this[b];},this);return this; +},include:function(b,c){if(this[b]==undefined){this[b]=c;}return this;},map:function(b,c){return new a(Object.map(this,b,c));},filter:function(b,c){return new a(Object.filter(this,b,c)); +},every:function(b,c){return Object.every(this,b,c);},some:function(b,c){return Object.some(this,b,c);},getKeys:function(){return Object.keys(this);},getValues:function(){return Object.values(this); +},toQueryString:function(b){return Object.toQueryString(this,b);}});a.alias({indexOf:"keyOf",contains:"hasValue"});})();Hash.implement({getFromPath:function(a){return Object.getFromPath(this,a); +},cleanValues:function(a){return new Hash(Object.cleanValues(this,a));},run:function(){Object.run(arguments);}});Element.implement({tidy:function(){this.set("value",this.get("value").tidy()); +},getTextInRange:function(b,a){return this.get("value").substring(b,a);},getSelectedText:function(){if(this.setSelectionRange){return this.getTextInRange(this.getSelectionStart(),this.getSelectionEnd()); +}return document.selection.createRange().text;},getSelectedRange:function(){if(this.selectionStart!=null){return{start:this.selectionStart,end:this.selectionEnd}; +}var e={start:0,end:0};var a=this.getDocument().selection.createRange();if(!a||a.parentElement()!=this){return e;}var c=a.duplicate();if(this.type=="text"){e.start=0-c.moveStart("character",-100000); +e.end=e.start+a.text.length;}else{var b=this.get("value");var d=b.length;c.moveToElementText(this);c.setEndPoint("StartToEnd",a);if(c.text.length){d-=b.match(/[\n\r]*$/)[0].length; +}e.end=d-c.text.length;c.setEndPoint("StartToStart",a);e.start=d-c.text.length;}return e;},getSelectionStart:function(){return this.getSelectedRange().start; +},getSelectionEnd:function(){return this.getSelectedRange().end;},setCaretPosition:function(a){if(a=="end"){a=this.get("value").length;}this.selectRange(a,a); +return this;},getCaretPosition:function(){return this.getSelectedRange().start;},selectRange:function(e,a){if(this.setSelectionRange){this.focus();this.setSelectionRange(e,a); +}else{var c=this.get("value");var d=c.substr(e,a-e).replace(/\r/g,"").length;e=c.substr(0,e).replace(/\r/g,"").length;var b=this.createTextRange();b.collapse(true); +b.moveEnd("character",e+d);b.moveStart("character",e);b.select();}return this;},insertAtCursor:function(b,a){var d=this.getSelectedRange();var c=this.get("value"); +this.set("value",c.substring(0,d.start)+b+c.substring(d.end,c.length));if(a!==false){this.selectRange(d.start,d.start+b.length);}else{this.setCaretPosition(d.start+b.length); +}return this;},insertAroundCursor:function(b,a){b=Object.append({before:"",defaultMiddle:"",after:""},b);var c=this.getSelectedText()||b.defaultMiddle; +var g=this.getSelectedRange();var f=this.get("value");if(g.start==g.end){this.set("value",f.substring(0,g.start)+b.before+c+b.after+f.substring(g.end,f.length)); +this.selectRange(g.start+b.before.length,g.end+b.before.length+c.length);}else{var d=f.substring(g.start,g.end);this.set("value",f.substring(0,g.start)+b.before+d+b.after+f.substring(g.end,f.length)); +var e=g.start+b.before.length;if(a!==false){this.selectRange(e,e+d.length);}else{this.setCaretPosition(e+f.length);}}return this;}});(function(){var b=function(e,d){var f=[]; +Object.each(d,function(g){Object.each(g,function(h){e.each(function(i){f.push(i+"-"+h+(i=="border"?"-width":""));});});});return f;};var c=function(f,e){var d=0; +Object.each(e,function(h,g){if(g.test(f)){d=d+h.toInt();}});return d;};var a=function(d){return !!(!d||d.offsetHeight||d.offsetWidth);};Element.implement({measure:function(h){if(a(this)){return h.call(this); +}var g=this.getParent(),e=[];while(!a(g)&&g!=document.body){e.push(g.expose());g=g.getParent();}var f=this.expose(),d=h.call(this);f();e.each(function(i){i(); +});return d;},expose:function(){if(this.getStyle("display")!="none"){return function(){};}var d=this.style.cssText;this.setStyles({display:"block",position:"absolute",visibility:"hidden"}); +return function(){this.style.cssText=d;}.bind(this);},getDimensions:function(d){d=Object.merge({computeSize:false},d);var i={x:0,y:0};var h=function(j,e){return(e.computeSize)?j.getComputedSize(e):j.getSize(); +};var f=this.getParent("body");if(f&&this.getStyle("display")=="none"){i=this.measure(function(){return h(this,d);});}else{if(f){try{i=h(this,d);}catch(g){}}}return Object.append(i,(i.x||i.x===0)?{width:i.x,height:i.y}:{x:i.width,y:i.height}); +},getComputedSize:function(d){if(d&&d.plains){d.planes=d.plains;}d=Object.merge({styles:["padding","border"],planes:{height:["top","bottom"],width:["left","right"]},mode:"both"},d); +var g={},e={width:0,height:0},f;if(d.mode=="vertical"){delete e.width;delete d.planes.width;}else{if(d.mode=="horizontal"){delete e.height;delete d.planes.height; +}}b(d.styles,d.planes).each(function(h){g[h]=this.getStyle(h).toInt();},this);Object.each(d.planes,function(i,h){var k=h.capitalize(),j=this.getStyle(h); +if(j=="auto"&&!f){f=this.getDimensions();}j=g[h]=(j=="auto")?f[h]:j.toInt();e["total"+k]=j;i.each(function(m){var l=c(m,g);e["computed"+m.capitalize()]=l; +e["total"+k]+=l;});},this);return Object.append(e,g);}});})();(function(){var a=false,b=false;var c=function(){var d=new Element("div").setStyles({position:"fixed",top:0,right:0}).inject(document.body); +a=(d.offsetTop===0);d.dispose();b=true;};Element.implement({pin:function(h,f){if(!b){c();}if(this.getStyle("display")=="none"){return this;}var j,k=window.getScroll(),l,e; +if(h!==false){j=this.getPosition(a?document.body:this.getOffsetParent());if(!this.retrieve("pin:_pinned")){var g={top:j.y-k.y,left:j.x-k.x};if(a&&!f){this.setStyle("position","fixed").setStyles(g); +}else{l=this.getOffsetParent();var i=this.getPosition(l),m=this.getStyles("left","top");if(l&&m.left=="auto"||m.top=="auto"){this.setPosition(i);}if(this.getStyle("position")=="static"){this.setStyle("position","absolute"); +}i={x:m.left.toInt()-k.x,y:m.top.toInt()-k.y};e=function(){if(!this.retrieve("pin:_pinned")){return;}var n=window.getScroll();this.setStyles({left:i.x+n.x,top:i.y+n.y}); +}.bind(this);this.store("pin:_scrollFixer",e);window.addEvent("scroll",e);}this.store("pin:_pinned",true);}}else{if(!this.retrieve("pin:_pinned")){return this; +}l=this.getParent();var d=(l.getComputedStyle("position")!="static"?l:l.getOffsetParent());j=this.getPosition(d);this.store("pin:_pinned",false);e=this.retrieve("pin:_scrollFixer"); +if(!e){this.setStyles({position:"absolute",top:j.y+k.y,left:j.x+k.x});}else{this.store("pin:_scrollFixer",null);window.removeEvent("scroll",e);}this.removeClass("isPinned"); +}return this;},unpin:function(){return this.pin(false);},togglePin:function(){return this.pin(!this.retrieve("pin:_pinned"));}});Element.alias("togglepin","togglePin"); +})();(function(b){var a=Element.Position={options:{relativeTo:document.body,position:{x:"center",y:"center"},offset:{x:0,y:0}},getOptions:function(d,c){c=Object.merge({},a.options,c); +a.setPositionOption(c);a.setEdgeOption(c);a.setOffsetOption(d,c);a.setDimensionsOption(d,c);return c;},setPositionOption:function(c){c.position=a.getCoordinateFromValue(c.position); +},setEdgeOption:function(d){var c=a.getCoordinateFromValue(d.edge);d.edge=c?c:(d.position.x=="center"&&d.position.y=="center")?{x:"center",y:"center"}:{x:"left",y:"top"}; +},setOffsetOption:function(f,d){var c={x:0,y:0},g=f.measure(function(){return document.id(this.getOffsetParent());}),e=g.getScroll();if(!g||g==f.getDocument().body){return; +}c=g.measure(function(){var i=this.getPosition();if(this.getStyle("position")=="fixed"){var h=window.getScroll();i.x+=h.x;i.y+=h.y;}return i;});d.offset={parentPositioned:g!=document.id(d.relativeTo),x:d.offset.x-c.x+e.x,y:d.offset.y-c.y+e.y}; +},setDimensionsOption:function(d,c){c.dimensions=d.getDimensions({computeSize:true,styles:["padding","border","margin"]});},getPosition:function(e,d){var c={}; +d=a.getOptions(e,d);var f=document.id(d.relativeTo)||document.body;a.setPositionCoordinates(d,c,f);if(d.edge){a.toEdge(c,d);}var g=d.offset;c.left=((c.x>=0||g.parentPositioned||d.allowNegative)?c.x:0).toInt(); +c.top=((c.y>=0||g.parentPositioned||d.allowNegative)?c.y:0).toInt();a.toMinMax(c,d);if(d.relFixedPosition||f.getStyle("position")=="fixed"){a.toRelFixedPosition(f,c); +}if(d.ignoreScroll){a.toIgnoreScroll(f,c);}if(d.ignoreMargins){a.toIgnoreMargins(c,d);}c.left=Math.ceil(c.left);c.top=Math.ceil(c.top);delete c.x;delete c.y; +return c;},setPositionCoordinates:function(k,g,d){var f=k.offset.y,h=k.offset.x,e=(d==document.body)?window.getScroll():d.getPosition(),j=e.y,c=e.x,i=window.getSize(); +switch(k.position.x){case"left":g.x=c+h;break;case"right":g.x=c+h+d.offsetWidth;break;default:g.x=c+((d==document.body?i.x:d.offsetWidth)/2)+h;break;}switch(k.position.y){case"top":g.y=j+f; +break;case"bottom":g.y=j+f+d.offsetHeight;break;default:g.y=j+((d==document.body?i.y:d.offsetHeight)/2)+f;break;}},toMinMax:function(c,d){var f={left:"x",top:"y"},e; +["minimum","maximum"].each(function(g){["left","top"].each(function(h){e=d[g]?d[g][f[h]]:null;if(e!=null&&((g=="minimum")?c[h]e)){c[h]=e;}});}); +},toRelFixedPosition:function(e,c){var d=window.getScroll();c.top+=d.y;c.left+=d.x;},toIgnoreScroll:function(e,d){var c=e.getScroll();d.top-=c.y;d.left-=c.x; +},toIgnoreMargins:function(c,d){c.left+=d.edge.x=="right"?d.dimensions["margin-right"]:(d.edge.x!="center"?-d.dimensions["margin-left"]:-d.dimensions["margin-left"]+((d.dimensions["margin-right"]+d.dimensions["margin-left"])/2)); +c.top+=d.edge.y=="bottom"?d.dimensions["margin-bottom"]:(d.edge.y!="center"?-d.dimensions["margin-top"]:-d.dimensions["margin-top"]+((d.dimensions["margin-bottom"]+d.dimensions["margin-top"])/2)); +},toEdge:function(c,d){var e={},g=d.dimensions,f=d.edge;switch(f.x){case"left":e.x=0;break;case"right":e.x=-g.x-g.computedRight-g.computedLeft;break;default:e.x=-(Math.round(g.totalWidth/2)); +break;}switch(f.y){case"top":e.y=0;break;case"bottom":e.y=-g.y-g.computedTop-g.computedBottom;break;default:e.y=-(Math.round(g.totalHeight/2));break;}c.x+=e.x; +c.y+=e.y;},getCoordinateFromValue:function(c){if(typeOf(c)!="string"){return c;}c=c.toLowerCase();return{x:c.test("left")?"left":(c.test("right")?"right":"center"),y:c.test(/upper|top/)?"top":(c.test("bottom")?"bottom":"center")}; +}};Element.implement({position:function(d){if(d&&(d.x!=null||d.y!=null)){return(b?b.apply(this,arguments):this);}var c=this.setStyle("position","absolute").calculatePosition(d); +return(d&&d.returnPos)?c:this.setStyles(c);},calculatePosition:function(c){return a.getPosition(this,c);}});})(Element.prototype.position);Element.implement({isDisplayed:function(){return this.getStyle("display")!="none"; +},isVisible:function(){var a=this.offsetWidth,b=this.offsetHeight;return(a==0&&b==0)?false:(a>0&&b>0)?true:this.style.display!="none";},toggle:function(){return this[this.isDisplayed()?"hide":"show"](); +},hide:function(){var b;try{b=this.getStyle("display");}catch(a){}if(b=="none"){return this;}return this.store("element:_originalDisplay",b||"").setStyle("display","none"); +},show:function(a){if(!a&&this.isDisplayed()){return this;}a=a||this.retrieve("element:_originalDisplay")||"block";return this.setStyle("display",(a=="none")?"block":a); +},swapClass:function(a,b){return this.removeClass(a).addClass(b);}});Document.implement({clearSelection:function(){if(window.getSelection){var a=window.getSelection(); +if(a&&a.removeAllRanges){a.removeAllRanges();}}else{if(document.selection&&document.selection.empty){try{document.selection.empty();}catch(b){}}}}});Class.Mutators.Binds=function(a){if(!this.prototype.initialize){this.implement("initialize",function(){}); +}return Array.from(a).concat(this.prototype.Binds||[]);};Class.Mutators.initialize=function(a){return function(){Array.from(this.Binds).each(function(b){var c=this[b]; +if(c){this[b]=c.bind(this);}},this);return a.apply(this,arguments);};};Class.Occlude=new Class({occlude:function(c,b){b=document.id(b||this.element);var a=b.retrieve(c||this.property); +if(a&&!this.occluded){return(this.occluded=a);}this.occluded=false;b.store(c||this.property,this);return this.occluded;}});var IframeShim=new Class({Implements:[Options,Events,Class.Occlude],options:{className:"iframeShim",src:'javascript:false;document.write("");',display:false,zIndex:null,margin:0,offset:{x:0,y:0},browsers:(Browser.ie6||(Browser.firefox&&Browser.version<3&&Browser.Platform.mac))},property:"IframeShim",initialize:function(b,a){this.element=document.id(b); +if(this.occlude()){return this.occluded;}this.setOptions(a);this.makeShim();return this;},makeShim:function(){if(this.options.browsers){var c=this.element.getStyle("zIndex").toInt(); +if(!c){c=1;var b=this.element.getStyle("position");if(b=="static"||!b){this.element.setStyle("position","relative");}this.element.setStyle("zIndex",c); +}c=((this.options.zIndex!=null||this.options.zIndex===0)&&c>this.options.zIndex)?this.options.zIndex:c-1;if(c<0){c=1;}this.shim=new Element("iframe",{src:this.options.src,scrolling:"no",frameborder:0,styles:{zIndex:c,position:"absolute",border:"none",filter:"progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)"},"class":this.options.className}).store("IframeShim",this); +var a=(function(){this.shim.inject(this.element,"after");this[this.options.display?"show":"hide"]();this.fireEvent("inject");}).bind(this);if(!IframeShim.ready){window.addEvent("load",a); +}else{a();}}else{this.position=this.hide=this.show=this.dispose=Function.from(this);}},position:function(){if(!IframeShim.ready||!this.shim){return this; +}var a=this.element.measure(function(){return this.getSize();});if(this.options.margin!=undefined){a.x=a.x-(this.options.margin*2);a.y=a.y-(this.options.margin*2); +this.options.offset.x+=this.options.margin;this.options.offset.y+=this.options.margin;}this.shim.set({width:a.x,height:a.y}).position({relativeTo:this.element,offset:this.options.offset}); +return this;},hide:function(){if(this.shim){this.shim.setStyle("display","none");}return this;},show:function(){if(this.shim){this.shim.setStyle("display","block"); +}return this.position();},dispose:function(){if(this.shim){this.shim.dispose();}return this;},destroy:function(){if(this.shim){this.shim.destroy();}return this; +}});window.addEvent("load",function(){IframeShim.ready=true;});var Mask=new Class({Implements:[Options,Events],Binds:["position"],options:{style:{},"class":"mask",maskMargins:false,useIframeShim:true,iframeShimOptions:{}},initialize:function(b,a){this.target=document.id(b)||document.id(document.body); +this.target.store("mask",this);this.setOptions(a);this.render();this.inject();},render:function(){this.element=new Element("div",{"class":this.options["class"],id:this.options.id||"mask-"+String.uniqueID(),styles:Object.merge({},this.options.style,{display:"none"}),events:{click:function(a){this.fireEvent("click",a); +if(this.options.hideOnClick){this.hide();}}.bind(this)}});this.hidden=true;},toElement:function(){return this.element;},inject:function(b,a){a=a||(this.options.inject?this.options.inject.where:"")||this.target==document.body?"inside":"after"; +b=b||(this.options.inject&&this.options.inject.target)||this.target;this.element.inject(b,a);if(this.options.useIframeShim){this.shim=new IframeShim(this.element,this.options.iframeShimOptions); +this.addEvents({show:this.shim.show.bind(this.shim),hide:this.shim.hide.bind(this.shim),destroy:this.shim.destroy.bind(this.shim)});}},position:function(){this.resize(this.options.width,this.options.height); +this.element.position({relativeTo:this.target,position:"topLeft",ignoreMargins:!this.options.maskMargins,ignoreScroll:this.target==document.body});return this; +},resize:function(a,e){var b={styles:["padding","border"]};if(this.options.maskMargins){b.styles.push("margin");}var d=this.target.getComputedSize(b);if(this.target==document.body){this.element.setStyles({width:0,height:0}); +var c=window.getScrollSize();if(d.totalHeight=0?a-1:0)).chain(d);}else{d();}return this;},detach:function(b){var a=function(c){c.removeEvent(this.options.trigger,c.retrieve("accordion:display")); +}.bind(this);if(!b){this.togglers.each(a);}else{a(b);}return this;},display:function(b,c){if(!this.check(b,c)){return this;}var h={},g=this.elements,a=this.options,f=this.effects; +if(c==null){c=true;}if(typeOf(b)=="element"){b=g.indexOf(b);}if(b==this.previous&&!a.alwaysHide){return this;}if(a.resetHeight){var e=g[this.previous]; +if(e&&!this.selfHidden){for(var d in f){e.setStyle(d,e[f[d]]);}}}if((this.timer&&a.link=="chain")||(b===this.previous&&!a.alwaysHide)){return this;}this.previous=b; +this.selfHidden=false;g.each(function(l,k){h[k]={};var j;if(k!=b){j=true;}else{if(a.alwaysHide&&((l.offsetHeight>0&&a.height)||l.offsetWidth>0&&a.width)){j=true; +this.selfHidden=true;}}this.fireEvent(j?"background":"active",[this.togglers[k],l]);for(var m in f){h[k][m]=j?0:l[f[m]];}if(!c&&!j&&a.resetHeight){h[k].height="auto"; +}},this);this.internalChain.clearChain();this.internalChain.chain(function(){if(a.resetHeight&&!this.selfHidden){var i=g[b];if(i){i.setStyle("height","auto"); +}}}.bind(this));return c?this.start(h):this.set(h).internalChain.callChain();}});var Accordion=new Class({Extends:Fx.Accordion,initialize:function(){this.parent.apply(this,arguments); +var a=Array.link(arguments,{container:Type.isElement});this.container=a.container;},addSection:function(c,b,e){c=document.id(c);b=document.id(b);var d=this.togglers.contains(c); +var a=this.togglers.length;if(a&&(!d||e)){e=e!=null?e:a-1;c.inject(this.togglers[e],"before");b.inject(c,"after");}else{if(this.container&&!d){c.inject(this.container); +b.inject(this.container);}}return this.parent.apply(this,arguments);}});Fx.Move=new Class({Extends:Fx.Morph,options:{relativeTo:document.body,position:"center",edge:false,offset:{x:0,y:0}},start:function(a){var b=this.element,c=b.getStyles("top","left"); +if(c.top=="auto"||c.left=="auto"){b.setPosition(b.getPosition(b.getOffsetParent()));}return this.parent(b.position(Object.merge({},this.options,a,{returnPos:true}))); +}});Element.Properties.move={set:function(a){this.get("move").cancel().setOptions(a);return this;},get:function(){var a=this.retrieve("move");if(!a){a=new Fx.Move(this,{link:"cancel"}); +this.store("move",a);}return a;}};Element.implement({move:function(a){this.get("move").start(a);return this;}});(function(){var a=function(d){var b=d.options.hideInputs; +if(window.OverText){var c=[null];OverText.each(function(e){c.include("."+e.options.labelClass);});if(c){b+=c.join(", ");}}return(b)?d.element.getElements(b):null; +};Fx.Reveal=new Class({Extends:Fx.Morph,options:{link:"cancel",styles:["padding","border","margin"],transitionOpacity:!Browser.ie6,mode:"vertical",display:function(){return this.element.get("tag")!="tr"?"block":"table-row"; +},opacity:1,hideInputs:Browser.ie?"select, input, textarea, object, embed":null},dissolve:function(){if(!this.hiding&&!this.showing){if(this.element.getStyle("display")!="none"){this.hiding=true; +this.showing=false;this.hidden=true;this.cssText=this.element.style.cssText;var d=this.element.getComputedSize({styles:this.options.styles,mode:this.options.mode}); +if(this.options.transitionOpacity){d.opacity=this.options.opacity;}var c={};Object.each(d,function(f,e){c[e]=[f,0];});this.element.setStyles({display:Function.from(this.options.display).call(this),overflow:"hidden"}); +var b=a(this);if(b){b.setStyle("visibility","hidden");}this.$chain.unshift(function(){if(this.hidden){this.hiding=false;this.element.style.cssText=this.cssText; +this.element.setStyle("display","none");if(b){b.setStyle("visibility","visible");}}this.fireEvent("hide",this.element);this.callChain();}.bind(this));this.start(c); +}else{this.callChain.delay(10,this);this.fireEvent("complete",this.element);this.fireEvent("hide",this.element);}}else{if(this.options.link=="chain"){this.chain(this.dissolve.bind(this)); +}else{if(this.options.link=="cancel"&&!this.hiding){this.cancel();this.dissolve();}}}return this;},reveal:function(){if(!this.showing&&!this.hiding){if(this.element.getStyle("display")=="none"){this.hiding=false; +this.showing=true;this.hidden=false;this.cssText=this.element.style.cssText;var d;this.element.measure(function(){d=this.element.getComputedSize({styles:this.options.styles,mode:this.options.mode}); +}.bind(this));if(this.options.heightOverride!=null){d.height=this.options.heightOverride.toInt();}if(this.options.widthOverride!=null){d.width=this.options.widthOverride.toInt(); +}if(this.options.transitionOpacity){this.element.setStyle("opacity",0);d.opacity=this.options.opacity;}var c={height:0,display:Function.from(this.options.display).call(this)}; +Object.each(d,function(f,e){c[e]=0;});c.overflow="hidden";this.element.setStyles(c);var b=a(this);if(b){b.setStyle("visibility","hidden");}this.$chain.unshift(function(){this.element.style.cssText=this.cssText; +this.element.setStyle("display",Function.from(this.options.display).call(this));if(!this.hidden){this.showing=false;}if(b){b.setStyle("visibility","visible"); +}this.callChain();this.fireEvent("show",this.element);}.bind(this));this.start(d);}else{this.callChain();this.fireEvent("complete",this.element);this.fireEvent("show",this.element); +}}else{if(this.options.link=="chain"){this.chain(this.reveal.bind(this));}else{if(this.options.link=="cancel"&&!this.showing){this.cancel();this.reveal(); +}}}return this;},toggle:function(){if(this.element.getStyle("display")=="none"){this.reveal();}else{this.dissolve();}return this;},cancel:function(){this.parent.apply(this,arguments); +if(this.cssText!=null){this.element.style.cssText=this.cssText;}this.hiding=false;this.showing=false;return this;}});Element.Properties.reveal={set:function(b){this.get("reveal").cancel().setOptions(b); +return this;},get:function(){var b=this.retrieve("reveal");if(!b){b=new Fx.Reveal(this);this.store("reveal",b);}return b;}};Element.Properties.dissolve=Element.Properties.reveal; +Element.implement({reveal:function(b){this.get("reveal").setOptions(b).reveal();return this;},dissolve:function(b){this.get("reveal").setOptions(b).dissolve(); +return this;},nix:function(b){var c=Array.link(arguments,{destroy:Type.isBoolean,options:Type.isObject});this.get("reveal").setOptions(b).dissolve().chain(function(){this[c.destroy?"destroy":"dispose"](); +}.bind(this));return this;},wink:function(){var c=Array.link(arguments,{duration:Type.isNumber,options:Type.isObject});var b=this.get("reveal").setOptions(c.options); +b.reveal().chain(function(){(function(){b.dissolve();}).delay(c.duration||2000);});}});})();(function(){Fx.Scroll=new Class({Extends:Fx,options:{offset:{x:0,y:0},wheelStops:true},initialize:function(c,b){this.element=this.subject=document.id(c); +this.parent(b);if(typeOf(this.element)!="element"){this.element=document.id(this.element.getDocument().body);}if(this.options.wheelStops){var d=this.element,e=this.cancel.pass(false,this); +this.addEvent("start",function(){d.addEvent("mousewheel",e);},true);this.addEvent("complete",function(){d.removeEvent("mousewheel",e);},true);}},set:function(){var b=Array.flatten(arguments); +if(Browser.firefox){b=[Math.round(b[0]),Math.round(b[1])];}this.element.scrollTo(b[0],b[1]);return this;},compute:function(d,c,b){return[0,1].map(function(e){return Fx.compute(d[e],c[e],b); +});},start:function(c,d){if(!this.check(c,d)){return this;}var b=this.element.getScroll();return this.parent([b.x,b.y],[c,d]);},calculateScroll:function(g,f){var d=this.element,b=d.getScrollSize(),h=d.getScroll(),j=d.getSize(),c=this.options.offset,i={x:g,y:f}; +for(var e in i){if(!i[e]&&i[e]!==0){i[e]=h[e];}if(typeOf(i[e])!="number"){i[e]=b[e]-j[e];}i[e]+=c[e];}return[i.x,i.y];},toTop:function(){return this.start.apply(this,this.calculateScroll(false,0)); +},toLeft:function(){return this.start.apply(this,this.calculateScroll(0,false));},toRight:function(){return this.start.apply(this,this.calculateScroll("right",false)); +},toBottom:function(){return this.start.apply(this,this.calculateScroll(false,"bottom"));},toElement:function(d,e){e=e?Array.from(e):["x","y"];var c=a(this.element)?{x:0,y:0}:this.element.getScroll(); +var b=Object.map(document.id(d).getPosition(this.element),function(g,f){return e.contains(f)?g+c[f]:false;});return this.start.apply(this,this.calculateScroll(b.x,b.y)); +},toElementEdge:function(d,g,e){g=g?Array.from(g):["x","y"];d=document.id(d);var i={},f=d.getPosition(this.element),j=d.getSize(),h=this.element.getScroll(),b=this.element.getSize(),c={x:f.x+j.x,y:f.y+j.y}; +["x","y"].each(function(k){if(g.contains(k)){if(c[k]>h[k]+b[k]){i[k]=c[k]-b[k];}if(f[k]this.elements.length){e.splice(this.elements.length-1,e.length-this.elements.length); +}}var b=0;i=a=0;e.each(function(k){var j={};if(d){j.top=i-f[k].top-b;i+=f[k].height;}else{j.left=a-f[k].left;a+=f[k].width;}b=b+f[k].margin;c[k]=j;},this); +var g={};Array.clone(e).sort().each(function(j){g[j]=c[j];});this.start(g);this.currentOrder=e;return this;},rearrangeDOM:function(a){a=a||this.currentOrder; +var b=this.elements[0].getParent();var c=[];this.elements.setStyle("opacity",0);a.each(function(d){c.push(this.elements[d].inject(b).setStyles({top:0,left:0})); +},this);this.elements.setStyle("opacity",1);this.elements=$$(c);this.setDefaultOrder();return this;},getDefaultOrder:function(){return this.elements.map(function(b,a){return a; +});},getCurrentOrder:function(){return this.currentOrder;},forward:function(){return this.sort(this.getDefaultOrder());},backward:function(){return this.sort(this.getDefaultOrder().reverse()); +},reverse:function(){return this.sort(this.currentOrder.reverse());},sortByElements:function(a){return this.sort(a.map(function(b){return this.elements.indexOf(b); +},this));},swap:function(c,b){if(typeOf(c)=="element"){c=this.elements.indexOf(c);}if(typeOf(b)=="element"){b=this.elements.indexOf(b);}var a=Array.clone(this.currentOrder); +a[this.currentOrder.indexOf(c)]=b;a[this.currentOrder.indexOf(b)]=c;return this.sort(a);}});var Drag=new Class({Implements:[Events,Options],options:{snap:6,unit:"px",grid:false,style:true,limit:false,handle:false,invert:false,preventDefault:false,stopPropagation:false,modifiers:{x:"left",y:"top"}},initialize:function(){var b=Array.link(arguments,{options:Type.isObject,element:function(c){return c!=null; +}});this.element=document.id(b.element);this.document=this.element.getDocument();this.setOptions(b.options||{});var a=typeOf(this.options.handle);this.handles=((a=="array"||a=="collection")?$$(this.options.handle):document.id(this.options.handle))||this.element; +this.mouse={now:{},pos:{}};this.value={start:{},now:{}};this.selection=(Browser.ie)?"selectstart":"mousedown";if(Browser.ie&&!Drag.ondragstartFixed){document.ondragstart=Function.from(false); +Drag.ondragstartFixed=true;}this.bound={start:this.start.bind(this),check:this.check.bind(this),drag:this.drag.bind(this),stop:this.stop.bind(this),cancel:this.cancel.bind(this),eventStop:Function.from(false)}; +this.attach();},attach:function(){this.handles.addEvent("mousedown",this.bound.start);return this;},detach:function(){this.handles.removeEvent("mousedown",this.bound.start); +return this;},start:function(a){var j=this.options;if(a.rightClick){return;}if(j.preventDefault){a.preventDefault();}if(j.stopPropagation){a.stopPropagation(); +}this.mouse.start=a.page;this.fireEvent("beforeStart",this.element);var c=j.limit;this.limit={x:[],y:[]};var e,g;for(e in j.modifiers){if(!j.modifiers[e]){continue; +}var b=this.element.getStyle(j.modifiers[e]);if(b&&!b.match(/px$/)){if(!g){g=this.element.getCoordinates(this.element.getOffsetParent());}b=g[j.modifiers[e]]; +}if(j.style){this.value.now[e]=(b||0).toInt();}else{this.value.now[e]=this.element[j.modifiers[e]];}if(j.invert){this.value.now[e]*=-1;}this.mouse.pos[e]=a.page[e]-this.value.now[e]; +if(c&&c[e]){var d=2;while(d--){var f=c[e][d];if(f||f===0){this.limit[e][d]=(typeof f=="function")?f():f;}}}}if(typeOf(this.options.grid)=="number"){this.options.grid={x:this.options.grid,y:this.options.grid}; +}var h={mousemove:this.bound.check,mouseup:this.bound.cancel};h[this.selection]=this.bound.eventStop;this.document.addEvents(h);},check:function(a){if(this.options.preventDefault){a.preventDefault(); +}var b=Math.round(Math.sqrt(Math.pow(a.page.x-this.mouse.start.x,2)+Math.pow(a.page.y-this.mouse.start.y,2)));if(b>this.options.snap){this.cancel();this.document.addEvents({mousemove:this.bound.drag,mouseup:this.bound.stop}); +this.fireEvent("start",[this.element,a]).fireEvent("snap",this.element);}},drag:function(b){var a=this.options;if(a.preventDefault){b.preventDefault(); +}this.mouse.now=b.page;for(var c in a.modifiers){if(!a.modifiers[c]){continue;}this.value.now[c]=this.mouse.now[c]-this.mouse.pos[c];if(a.invert){this.value.now[c]*=-1; +}if(a.limit&&this.limit[c]){if((this.limit[c][1]||this.limit[c][1]===0)&&(this.value.now[c]>this.limit[c][1])){this.value.now[c]=this.limit[c][1];}else{if((this.limit[c][0]||this.limit[c][0]===0)&&(this.value.now[c]d.left&&b.xd.top);},this).getLast();if(this.overed!=a){if(this.overed){this.fireEvent("leave",[this.element,this.overed]); +}if(a){this.fireEvent("enter",[this.element,a]);}this.overed=a;}},drag:function(a){this.parent(a);if(this.options.checkDroppables&&this.droppables.length){this.checkDroppables(); +}},stop:function(a){this.checkDroppables();this.fireEvent("drop",[this.element,this.overed,a]);this.overed=null;return this.parent(a);}});Element.implement({makeDraggable:function(a){var b=new Drag.Move(this,a); +this.store("dragger",b);return b;}});var Slider=new Class({Implements:[Events,Options],Binds:["clickedElement","draggedKnob","scrolledElement"],options:{onTick:function(a){this.setKnobPosition(a); +},initialStep:0,snap:false,offset:0,range:false,wheel:false,steps:100,mode:"horizontal"},initialize:function(f,a,e){this.setOptions(e);e=this.options;this.element=document.id(f); +a=this.knob=document.id(a);this.previousChange=this.previousEnd=this.step=-1;var b={},d={x:false,y:false};switch(e.mode){case"vertical":this.axis="y";this.property="top"; +this.offset="offsetHeight";break;case"horizontal":this.axis="x";this.property="left";this.offset="offsetWidth";}this.setSliderDimensions();this.setRange(e.range); +if(a.getStyle("position")=="static"){a.setStyle("position","relative");}a.setStyle(this.property,-e.offset);d[this.axis]=this.property;b[this.axis]=[-e.offset,this.full-e.offset]; +var c={snap:0,limit:b,modifiers:d,onDrag:this.draggedKnob,onStart:this.draggedKnob,onBeforeStart:(function(){this.isDragging=true;}).bind(this),onCancel:function(){this.isDragging=false; +}.bind(this),onComplete:function(){this.isDragging=false;this.draggedKnob();this.end();}.bind(this)};if(e.snap){this.setSnap(c);}this.drag=new Drag(a,c); +this.attach();if(e.initialStep!=null){this.set(e.initialStep);}},attach:function(){this.element.addEvent("mousedown",this.clickedElement);if(this.options.wheel){this.element.addEvent("mousewheel",this.scrolledElement); +}this.drag.attach();return this;},detach:function(){this.element.removeEvent("mousedown",this.clickedElement).removeEvent("mousewheel",this.scrolledElement); +this.drag.detach();return this;},autosize:function(){this.setSliderDimensions().setKnobPosition(this.toPosition(this.step));this.drag.options.limit[this.axis]=[-this.options.offset,this.full-this.options.offset]; +if(this.options.snap){this.setSnap();}return this;},setSnap:function(a){if(!a){a=this.drag.options;}a.grid=Math.ceil(this.stepWidth);a.limit[this.axis][1]=this.full; +return this;},setKnobPosition:function(a){if(this.options.snap){a=this.toPosition(this.step);}this.knob.setStyle(this.property,a);return this;},setSliderDimensions:function(){this.full=this.element.measure(function(){this.half=this.knob[this.offset]/2; +return this.element[this.offset]-this.knob[this.offset]+(this.options.offset*2);}.bind(this));return this;},set:function(a){if(!((this.range>0)^(a0)^(a>this.max))){a=this.max;}this.step=Math.round(a);return this.checkStep().fireEvent("tick",this.toPosition(this.step)).end();},setRange:function(a,b){this.min=Array.pick([a[0],0]); +this.max=Array.pick([a[1],this.options.steps]);this.range=this.max-this.min;this.steps=this.options.steps||this.full;this.stepSize=Math.abs(this.range)/this.steps; +this.stepWidth=this.stepSize*this.full/Math.abs(this.range);if(a){this.set(Array.pick([b,this.step]).floor(this.min).max(this.max));}return this;},clickedElement:function(c){if(this.isDragging||c.target==this.knob){return; +}var b=this.range<0?-1:1,a=c.page[this.axis]-this.element.getPosition()[this.axis]-this.half;a=a.limit(-this.options.offset,this.full-this.options.offset); +this.step=Math.round(this.min+b*this.toStep(a));this.checkStep().fireEvent("tick",a).end();},scrolledElement:function(a){var b=(this.options.mode=="horizontal")?(a.wheel<0):(a.wheel>0); +this.set(this.step+(b?-1:1)*this.stepSize);a.stop();},draggedKnob:function(){var b=this.range<0?-1:1,a=this.drag.value.now[this.axis];a=a.limit(-this.options.offset,this.full-this.options.offset); +this.step=Math.round(this.min+b*this.toStep(a));this.checkStep();},checkStep:function(){var a=this.step;if(this.previousChange!=a){this.previousChange=a; +this.fireEvent("change",a);}return this;},end:function(){var a=this.step;if(this.previousEnd!==a){this.previousEnd=a;this.fireEvent("complete",a+"");}return this; +},toStep:function(a){var b=(a+this.options.offset)*this.stepSize/this.full*this.steps;return this.options.steps?Math.round(b-=b%this.stepSize):b;},toPosition:function(a){return(this.full*Math.abs(this.min-a))/(this.steps*this.stepSize)-this.options.offset; +}});var Sortables=new Class({Implements:[Events,Options],options:{opacity:1,clone:false,revert:false,handle:false,dragOptions:{},snap:4,constrain:false,preventDefault:false},initialize:function(a,b){this.setOptions(b); +this.elements=[];this.lists=[];this.idle=true;this.addLists($$(document.id(a)||a));if(!this.options.clone){this.options.revert=false;}if(this.options.revert){this.effect=new Fx.Morph(null,Object.merge({duration:250,link:"cancel"},this.options.revert)); +}},attach:function(){this.addLists(this.lists);return this;},detach:function(){this.lists=this.removeLists(this.lists);return this;},addItems:function(){Array.flatten(arguments).each(function(a){this.elements.push(a); +var b=a.retrieve("sortables:start",function(c){this.start.call(this,c,a);}.bind(this));(this.options.handle?a.getElement(this.options.handle)||a:a).addEvent("mousedown",b); +},this);return this;},addLists:function(){Array.flatten(arguments).each(function(a){this.lists.include(a);this.addItems(a.getChildren());},this);return this; +},removeItems:function(){return $$(Array.flatten(arguments).map(function(a){this.elements.erase(a);var b=a.retrieve("sortables:start");(this.options.handle?a.getElement(this.options.handle)||a:a).removeEvent("mousedown",b); +return a;},this));},removeLists:function(){return $$(Array.flatten(arguments).map(function(a){this.lists.erase(a);this.removeItems(a.getChildren());return a; +},this));},getClone:function(b,a){if(!this.options.clone){return new Element(a.tagName).inject(document.body);}if(typeOf(this.options.clone)=="function"){return this.options.clone.call(this,b,a,this.list); +}var c=a.clone(true).setStyles({margin:0,position:"absolute",visibility:"hidden",width:a.getStyle("width")}).addEvent("mousedown",function(d){a.fireEvent("mousedown",d); +});if(c.get("html").test("radio")){c.getElements("input[type=radio]").each(function(d,e){d.set("name","clone_"+e);if(d.get("checked")){a.getElements("input[type=radio]")[e].set("checked",true); +}});}return c.inject(this.list).setPosition(a.getPosition(a.getOffsetParent()));},getDroppables:function(){var a=this.list.getChildren().erase(this.clone).erase(this.element); +if(!this.options.constrain){a.append(this.lists).erase(this.list);}return a;},insert:function(c,b){var a="inside";if(this.lists.contains(b)){this.list=b; +this.drag.droppables=this.getDroppables();}else{a=this.element.getAllPrevious().contains(b)?"before":"after";}this.element.inject(b,a);this.fireEvent("sort",[this.element,this.clone]); +},start:function(b,a){if(!this.idle||b.rightClick||["button","input","a","textarea"].contains(b.target.get("tag"))){return;}this.idle=false;this.element=a; +this.opacity=a.getStyle("opacity");this.list=a.getParent();this.clone=this.getClone(b,a);this.drag=new Drag.Move(this.clone,Object.merge({preventDefault:this.options.preventDefault,snap:this.options.snap,container:this.options.constrain&&this.element.getParent(),droppables:this.getDroppables()},this.options.dragOptions)).addEvents({onSnap:function(){b.stop(); +this.clone.setStyle("visibility","visible");this.element.setStyle("opacity",this.options.opacity||0);this.fireEvent("start",[this.element,this.clone]); +}.bind(this),onEnter:this.insert.bind(this),onCancel:this.end.bind(this),onComplete:this.end.bind(this)});this.clone.inject(this.element,"before");this.drag.start(b); +},end:function(){this.drag.detach();this.element.setStyle("opacity",this.opacity);if(this.effect){var b=this.element.getStyles("width","height"),d=this.clone,c=d.computePosition(this.element.getPosition(this.clone.getOffsetParent())); +var a=function(){this.removeEvent("cancel",a);d.destroy();};this.effect.element=d;this.effect.start({top:c.top,left:c.left,width:b.width,height:b.height,opacity:0.25}).addEvent("cancel",a).chain(a); +}else{this.clone.destroy();}this.reset();},reset:function(){this.idle=true;this.fireEvent("complete",this.element);},serialize:function(){var c=Array.link(arguments,{modifier:Type.isFunction,index:function(d){return d!=null; +}});var b=this.lists.map(function(d){return d.getChildren().map(c.modifier||function(e){return e.get("id");},this);},this);var a=c.index;if(this.lists.length==1){a=0; +}return(a||a===0)&&a>=0&&a=3){d="rgb";c=Array.slice(arguments,0,3);}else{if(typeof c=="string"){if(c.match(/rgb/)){c=c.rgbToHex().hexToRgb(true); +}else{if(c.match(/hsb/)){c=c.hsbToRgb();}else{c=c.hexToRgb(true);}}}}d=d||"rgb";switch(d){case"hsb":var b=c;c=c.hsbToRgb();c.hsb=b;break;case"hex":c=c.hexToRgb(true); +break;}c.rgb=c.slice(0,3);c.hsb=c.hsb||c.rgbToHsb();c.hex=c.rgbToHex();return Object.append(c,this);});a.implement({mix:function(){var b=Array.slice(arguments); +var d=(typeOf(b.getLast())=="number")?b.pop():50;var c=this.slice();b.each(function(e){e=new a(e);for(var f=0;f<3;f++){c[f]=Math.round((c[f]/100*(100-d))+(e[f]/100*d)); +}});return new a(c,"rgb");},invert:function(){return new a(this.map(function(b){return 255-b;}));},setHue:function(b){return new a([b,this.hsb[1],this.hsb[2]],"hsb"); +},setSaturation:function(b){return new a([this.hsb[0],b,this.hsb[2]],"hsb");},setBrightness:function(b){return new a([this.hsb[0],this.hsb[1],b],"hsb"); +}});this.$RGB=function(e,d,c){return new a([e,d,c],"rgb");};this.$HSB=function(e,d,c){return new a([e,d,c],"hsb");};this.$HEX=function(b){return new a(b,"hex"); +};Array.implement({rgbToHsb:function(){var c=this[0],d=this[1],k=this[2],h=0;var j=Math.max(c,d,k),f=Math.min(c,d,k);var l=j-f;var i=j/255,g=(j!=0)?l/j:0; +if(g!=0){var e=(j-c)/l;var b=(j-d)/l;var m=(j-k)/l;if(c==j){h=m-b;}else{if(d==j){h=2+e-m;}else{h=4+b-e;}}h/=6;if(h<0){h++;}}return[Math.round(h*360),Math.round(g*100),Math.round(i*100)]; +},hsbToRgb:function(){var d=Math.round(this[2]/100*255);if(this[1]==0){return[d,d,d];}else{var b=this[0]%360;var g=b%60;var h=Math.round((this[2]*(100-this[1]))/10000*255); +var e=Math.round((this[2]*(6000-this[1]*g))/600000*255);var c=Math.round((this[2]*(6000-this[1]*(60-g)))/600000*255);switch(Math.floor(b/60)){case 0:return[d,c,h]; +case 1:return[e,d,h];case 2:return[h,d,c];case 3:return[h,e,d];case 4:return[c,h,d];case 5:return[d,h,e];}}return false;}});String.implement({rgbToHsb:function(){var b=this.match(/\d{1,3}/g); +return(b)?b.rgbToHsb():null;},hsbToRgb:function(){var b=this.match(/\d{1,3}/g);return(b)?b.hsbToRgb():null;}});})();(function(){this.Group=new Class({initialize:function(){this.instances=Array.flatten(arguments); +},addEvent:function(e,d){var g=this.instances,a=g.length,f=a,c=new Array(a),b=this;g.each(function(h,j){h.addEvent(e,function(){if(!c[j]){f--;}c[j]=arguments; +if(!f){d.call(b,g,h,c);f=a;c=new Array(a);}});});}});})();Locale.define("en-GB","Date",{dateOrder:["date","month","year"],shortDate:"%d/%m/%Y",shortTime:"%H:%M"}).inherit("en-US","Date"); diff --git a/templates/default/js/userbar.js b/templates/default/js/userbar.js new file mode 100755 index 0000000..67e2757 --- /dev/null +++ b/templates/default/js/userbar.js @@ -0,0 +1 @@ +/* Nothing yet */ diff --git a/templates/default/lightface/LightFace.Request.js b/templates/default/lightface/LightFace.Request.js new file mode 100755 index 0000000..0d4aead --- /dev/null +++ b/templates/default/lightface/LightFace.Request.js @@ -0,0 +1,59 @@ +/* +--- +description: LightFace.Request + +authors: + - David Walsh (http://davidwalsh.name) + +license: + - MIT-style license + +requires: + core/1.2.1: "*" + +provides: + - LightFace.Request +... +*/ +LightFace.Request = new Class({ + Extends: LightFace, + options: { + url: "", + request: { + url: false + } + }, + initialize: function(options) { + this.parent(options); + if(this.options.url) this.load(); + }, + load: function(url, title) { + var props = (Object.append || $extend)({ + onRequest: function() { + this.fade(); + this.fireEvent("request"); + }.bind(this), + onSuccess: function(response) { + this.messageBox.set("html", response); + this.fireEvent("success"); + }.bind(this), + onFailure: function() { + this.messageBox.set("html", this.options.errorMessage); + this.fireEvent("failure"); + }.bind(this), + onComplete: function() { + this._resize(); + this._ie6Size(); + this.messageBox.setStyle("opacity", 1); + this.unfade(); + this.fireEvent("complete"); + }.bind(this) + },this.options.request); + + if(title && this.title) this.title.set("html", title); + if(!props.url) props.url = url || this.options.url; + + new Request(props).send(); + return this; + } +}); \ No newline at end of file diff --git a/templates/default/lightface/LightFace.css b/templates/default/lightface/LightFace.css new file mode 100755 index 0000000..332295b --- /dev/null +++ b/templates/default/lightface/LightFace.css @@ -0,0 +1,90 @@ + +.lightfaceContent { + top: -9000px; + left: -9000px; + border-radius: 4px; + background-color: #fff; + border: 1px solid #ddd; + -moz-box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + -webkit-box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + box-shadow: 3px 3px 4px rgba(0,0,0,0.6); + position: absolute; +} + +.loading { + display: block; + margin: 10px auto; +} + +.lightfaceContent .lightfaceTitle { + font-size: 14px; + color: #555; + font-weight: bold; + margin: -1px; + margin-bottom: 0; + padding: 5px 10px; +} + +.lightfaceContent .lightfaceDraggable { + cursor:move; +} + +.lightfaceContent .lightfaceMessage { + overflow: auto; + margin: 0; + position: relative; + padding: 5px 10px; +} + +.lightfaceContent .lightfaceMessage h3, +.lightfaceContent .lightfaceMessage h4, +.lightfaceContent .lightfaceMessage h5, +.lightfaceContent .lightfaceMessage h6 { + margin-top: 6px; +} + +.lightfaceContent .lightfaceFooter { + padding: 6px 10px; + text-align: right; + background: #f4f4f4; +} + +.hiddenButton { + visibility: hidden; +} + +.lightfaceOverlay { + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + /* + background-image: url(images/fbloader.gif); + background-position: center center; + background-repeat: no-repeat; + background-color: #fff; + */ + background: #444; + opacity: 0.5; +} + +.lightfaceMessageBox { + overflow: auto; + padding: 5px 10px; + min-height: 20px; + position:relative; + border-top: 1px solid #eee; + color: #000; + background: #f4f4f4; +} + +.lightFaceMessageBoxImage { + overflow: hidden; + padding: 0; + background:url(images/fbloader.gif) center center no-repeat #fff; +} + +.lightFaceMessageBoxImage img { + display: block; +} \ No newline at end of file diff --git a/templates/default/lightface/LightFace.js b/templates/default/lightface/LightFace.js new file mode 100755 index 0000000..a0cb3b5 --- /dev/null +++ b/templates/default/lightface/LightFace.js @@ -0,0 +1,339 @@ +/* +--- +description: LightFace + +license: MIT-style + +authors: +- David Walsh (http://davidwalsh.name) + +requires: +- core/1.2.1: "*" + +provides: [LightFace] + +... +*/ + +var LightFace = new Class({ + + Implements: [Options,Events], + + options: { + width: "auto", + height: "auto", + draggable: false, + title: "", + buttons: [], + fadeDelay: 400, + fadeDuration: 400, + keys: { + esc: function() { this.close(); } + }, + content: "

    Message not specified.

    ", + zIndex: 9001, + pad: 100, + overlayAll: false, + constrain: false, + resetOnScroll: true, + baseClass: "lightface", + errorMessage: "

    The requested file could not be found.

    "/*, + onOpen: $empty, + onClose: $empty, + onFade: $empty, + onUnfade: $empty, + onComplete: $empty, + onRequest: $empty, + onSuccess: $empty, + onFailure: $empty + */ + }, + + + initialize: function(options) { + this.setOptions(options); + this.state = false; + this.buttons = {}; + this.resizeOnOpen = true; + this.ie6 = typeof document.body.style.maxHeight == "undefined"; + this.draw(); + }, + + draw: function() { + + //create main box + this.box = new Element("table",{ + "class": this.options.baseClass, + styles: { + "z-index": this.options.zIndex, + opacity: 0 + }, + tween: { + duration: this.options.fadeDuration, + onComplete: function() { + if(this.box.getStyle("opacity") == 0) { + this.box.setStyles({ top: -9000, left: -9000 }); + } + }.bind(this) + } + }).inject(document.body,"bottom"); + + //draw rows and cells; use native JS to avoid IE7 and I6 offsetWidth and offsetHeight issues + var verts = ["top","center","bottom"], hors = ["Left","Center","Right"], len = verts.length; + for(var x = 0; x < len; x++) { + var row = this.box.insertRow(x); + for(var y = 0; y < len; y++) { + var cssClass = verts[x] + hors[y], cell = row.insertCell(y); + cell.className = cssClass; + if (cssClass == "centerCenter") { + this.contentBox = new Element("div",{ + "class": "lightfaceContent", + styles: { + width: this.options.width + } + }); + cell.appendChild(this.contentBox); + } + else { + document.id(cell).setStyle("opacity", 0.4); + } + } + } + + //draw title + + this.title = new Element("h2",{ + "class": "lightfaceTitle", + html: this.options.title + }).inject(this.contentBox); + + if(this.options.draggable && window["Drag"] != null) { + this.draggable = true; + new Drag(this.box, { handle: this.title }); + this.title.addClass("lightfaceDraggable"); + } + + //draw message box + this.messageBox = new Element("div", { + "class": "lightfaceMessageBox", + html: this.options.content || "", + styles: { + height: this.options.height + } + }).inject(this.contentBox); + + //button container + this.footer = new Element("div", { + "class": "lightfaceFooter", + styles: { + display: "none" + } + }).inject(this.contentBox); + + //draw overlay + this.overlay = new Element("div", { + html: " ", + styles: { + opacity: 0, + visibility: "hidden", + "z-index": this.options.zIndex - 1, // force the overlay under the box + }, + "class": "lightfaceOverlay", + tween: { + duration: this.options.fadeDuration, + onComplete: function() { + if(this.overlay.getStyle("opacity") == 0) { + // Rehide the overlay when it is transparent + this.overlay.setStyle('visibility', 'hidden'); + } + }.bind(this) + } + }).inject(document.body, 'bottom'); + if(!this.options.overlayAll) { + this.overlay.setStyle("top", (this.title ? this.title.getSize().y - 1: 0)); + } + + //create initial buttons + this.buttons = []; + if(this.options.buttons.length) { + this.options.buttons.each(function(button) { + this.addButton(button.title, button.event, button.color); + },this); + } + + //focus node + this.focusNode = this.box; + + return this; + }, + + // Manage buttons + addButton: function(title,clickEvent,color) { + this.footer.setStyle("display", "block"); + var focusClass = "lightfacefocus" + color; + this.buttons[title] = (new Element("input", { + "class": color ? "button "+color : "button", + type: "button", + value: title, + events: { + click: (clickEvent || this.close).bind(this) + } + }).inject(this.footer)); + return this; + }, + showButton: function(title) { + if(this.buttons[title]) this.buttons[title].removeClass("hiddenButton"); + return this.buttons[title]; + }, + hideButton: function(title) { + if(this.buttons[title]) this.buttons[title].addClass("hiddenButton"); + return this.buttons[title]; + }, + + // Open and close box + close: function(fast) { + if(this.isOpen) { + this.box[fast ? "setStyles" : "tween"]("opacity", 0); + this.overlay[fast ? "setStyles" : "tween"]("opacity", 0); + this.fireEvent("close"); + this._detachEvents(); + this.isOpen = false; + } + return this; + }, + + open: function(fast) { + if(!this.isOpen) { + this.overlay[fast ? "setStyles" : "tween"]("opacity", 0.4); + this.overlay.setStyle("visibility", 'visible'); + this.box[fast ? "setStyles" : "tween"]("opacity", 1); + if(this.resizeOnOpen) this._resize(); + this.fireEvent("open"); + this._attachEvents(); + (function() { + this._setFocus(); + }).bind(this).delay(this.options.fadeDuration + 10); + this.isOpen = true; + } + return this; + }, + + _setFocus: function() { + this.focusNode.setAttribute("tabIndex", 0); + this.focusNode.focus(); + }, + + // Show and hide overlay + fade: function(fade, delay) { + this._ie6Size(); + (function() { + this.overlay.setStyle("opacity", fade || 1); + }.bind(this)).delay(delay || 0); + this.fireEvent("fade"); + return this; + }, + unfade: function(delay) { + (function() { + this.overlay.fade(0); + }.bind(this)).delay(delay || this.options.fadeDelay); + this.fireEvent("unfade"); + return this; + }, + _ie6Size: function() { + if(this.ie6) { + var size = this.contentBox.getSize(); + var titleHeight = (this.options.overlayAll || !this.title) ? 0 : this.title.getSize().y; + this.overlay.setStyles({ + height: size.y - titleHeight, + width: size.x + }); + } + }, + + // Loads content + load: function(content, title) { + if(content) this.messageBox.set("html", content); + title = title || this.options.title; + if(title) this.title.set("html", title).setStyle("display", "block"); + else this.title.setStyle("display", "none"); + this.fireEvent("complete"); + return this; + }, + + // Attaches events when opened + _attachEvents: function() { + this.keyEvent = function(e){ + if(this.options.keys[e.key]) this.options.keys[e.key].call(this); + }.bind(this); + this.focusNode.addEvent("keyup", this.keyEvent); + + this.resizeEvent = this.options.constrain ? function(e) { + this._resize(); + }.bind(this) : function() { + this._position(); + }.bind(this); + window.addEvent("resize", this.resizeEvent); + + if(this.options.resetOnScroll) { + this.scrollEvent = function() { + this._position(); + }.bind(this); + window.addEvent("scroll", this.scrollEvent); + } + + return this; + }, + + // Detaches events upon close + _detachEvents: function() { + this.focusNode.removeEvent("keyup", this.keyEvent); + window.removeEvent("resize", this.resizeEvent); + if(this.scrollEvent) window.removeEvent("scroll", this.scrollEvent); + return this; + }, + + // Repositions the box + _position: function() { + var windowSize = window.getSize(), + scrollSize = window.getScroll(), + boxSize = this.box.getSize(); + this.box.setStyles({ + left: scrollSize.x + ((windowSize.x - boxSize.x) / 2), + top: scrollSize.y + ((windowSize.y - boxSize.y) / 2) + }); + this._ie6Size(); + return this; + }, + + // Resizes the box, then positions it + _resize: function() { + var height = this.options.height; + if(height == "auto") { + //get the height of the content box + var max = window.getSize().y - this.options.pad; + if(this.contentBox.getSize().y > max) height = max; + } + this.messageBox.setStyle("height", height); + this._position(); + }, + + // Expose message box + toElement: function () { + return this.messageBox; + }, + + // Expose entire modal box + getBox: function() { + return this.box; + }, + + // Cleanup + destroy: function() { + this._detachEvents(); + this.buttons.each(function(button) { + button.removeEvents("click"); + }); + this.box.dispose(); + delete this.box; + } +}); \ No newline at end of file diff --git a/templates/default/lightface/LightFaceMod.js b/templates/default/lightface/LightFaceMod.js new file mode 100755 index 0000000..b187bf3 --- /dev/null +++ b/templates/default/lightface/LightFaceMod.js @@ -0,0 +1,332 @@ +/* +--- +description: LightFace + +license: MIT-style + +authors: +- David Walsh (http://davidwalsh.name) +- Chris Page (http://starforge.co.uk/) + +requires: +- core/1.2.1: "*" + +provides: [LightFace] + +... +*/ + +var LightFace = new Class({ + + Implements: [Options,Events], + + options: { + width: "auto", + height: "auto", + draggable: false, + title: "", + buttons: [], + fadeDelay: 400, + fadeDuration: 400, + keys: { + esc: function() { this.close(); } + }, + content: "

    Message not specified.

    ", + zIndex: 9001, + pad: 100, + overlayAll: false, + constrain: false, + resetOnScroll: true, + baseClass: "lightface", + errorMessage: "

    The requested file could not be found.

    "/*, + onOpen: $empty, + onClose: $empty, + onFade: $empty, + onUnfade: $empty, + onComplete: $empty, + onRequest: $empty, + onSuccess: $empty, + onFailure: $empty + */ + }, + + + initialize: function(options) { + this.setOptions(options); + this.state = false; + this.buttons = {}; + this.resizeOnOpen = true; + this.ie6 = typeof document.body.style.maxHeight == "undefined"; + this.draw(); + }, + + draw: function() { + + //create main box + this.contentBox = this.box = new Element("div", + { + "class": "lightfaceContent", + styles: { + "z-index": this.options.zIndex, + width: this.options.width + }, + tween: { + duration: this.options.fadeDuration, + onComplete: function() { + if(this.box.getStyle("opacity") < 0.2) { + this.box.setStyles({ top: -9000, left: -9000 }); + } + }.bind(this) + } + }).inject(document.body, "bottom"); + + //draw title + this.title = new Element("h2",{ + "class": "lightfaceTitle", + html: this.options.title + }).inject(this.contentBox); + + if(this.options.draggable && window["Drag"] != null) { + this.draggable = true; + new Drag(this.box, { handle: this.title }); + this.title.addClass("lightfaceDraggable"); + } + + //draw message box + this.messageBox = new Element("div", { + "class": "lightfaceMessageBox", + html: this.options.content || "", + styles: { + height: this.options.height + } + }).inject(this.contentBox); + + //button container + this.footer = new Element("div", { + "class": "lightfaceFooter", + styles: { + display: "none" + } + }).inject(this.contentBox); + + //draw overlay + this.overlay = new Element("div", { + html: " ", + styles: { + opacity: 0, + visibility: "hidden", + "z-index": this.options.zIndex - 1, // force the overlay under the box + }, + "class": "lightfaceOverlay", + tween: { + duration: this.options.fadeDuration, + onComplete: function() { + if(this.overlay.getStyle("opacity") == 0) { + // Rehide the overlay when it is transparent + this.overlay.setStyle('visibility', 'hidden'); + } + }.bind(this) + } + }).inject(document.body, 'bottom'); + if(!this.options.overlayAll) { + this.overlay.setStyle("top", (this.title ? this.title.getSize().y - 1: 0)); + } + + //create initial buttons + this.buttons = []; + if(this.options.buttons.length) { + this.options.buttons.each(function(button) { + this.addButton(button.title, button.event, button.color); + },this); + } + + //focus node + this.focusNode = this.box; + + return this; + }, + + // Manage buttons + addButton: function(title,clickEvent,color) { + this.footer.setStyle("display", "block"); + var focusClass = "lightfacefocus" + color; + this.buttons.push(new Element("input", { + "class": color ? "button "+color : "button", + type: "button", + value: title, + events: { + click: (clickEvent || this.close).bind(this) + } + }).inject(this.footer)); + return this; + }, + + // Remove any existing buttons from the bar + clearButtons: function() { + this.buttons.each(function(element) { + element.removeEvents('click'); + element.destroy(); + }); + this.buttons.empty(); + this.footer.setStyle("display", "none"); + }, + // Set the buttons for the window to the specified ones (is effectively a + // clear if newbuttons is null or an empty object) + setButtons: function(newbuttons) { + this.clearButtons(); + + if(newbuttons && newbuttons.length) { + newbuttons.each(function(button) { + this.addButton(button.title, button.event, button.color); + },this); + } + }, + + + // Open and close box + close: function(fast) { + if(this.isOpen) { + this.box[fast ? "setStyles" : "tween"]("opacity", 0); + this.overlay[fast ? "setStyles" : "tween"]("opacity", 0); + this.fireEvent("close"); + this._detachEvents(); + this.isOpen = false; + } + return this; + }, + + open: function(fast) { + if(!this.isOpen) { + this.overlay[fast ? "setStyles" : "tween"]("opacity", 0.4); + this.overlay.setStyle("visibility", 'visible'); + this.box[fast ? "setStyles" : "tween"]("opacity", 1); + if(this.resizeOnOpen) this._resize(); + this.fireEvent("open"); + this._attachEvents(); + (function() { + this._setFocus(); + }).bind(this).delay(this.options.fadeDuration + 10); + this.isOpen = true; + } + return this; + }, + + _setFocus: function() { + this.focusNode.setAttribute("tabIndex", 0); + this.focusNode.focus(); + }, + + // Show and hide overlay + fade: function(fade, delay) { + this._ie6Size(); + (function() { + this.overlay.setStyle("opacity", fade || 1); + }.bind(this)).delay(delay || 0); + this.fireEvent("fade"); + return this; + }, + unfade: function(delay) { + (function() { + this.overlay.fade(0); + }.bind(this)).delay(delay || this.options.fadeDelay); + this.fireEvent("unfade"); + return this; + }, + _ie6Size: function() { + if(this.ie6) { + var size = this.contentBox.getSize(); + var titleHeight = (this.options.overlayAll || !this.title) ? 0 : this.title.getSize().y; + this.overlay.setStyles({ + height: size.y - titleHeight, + width: size.x + }); + } + }, + + // Loads content + load: function(content, title) { + if(content) this.messageBox.set("html", content); + title = title || this.options.title; + if(title) this.title.set("html", title).setStyle("display", "block"); + else this.title.setStyle("display", "none"); + this.fireEvent("complete"); + return this; + }, + + // Attaches events when opened + _attachEvents: function() { + this.keyEvent = function(e){ + if(this.options.keys[e.key]) this.options.keys[e.key].call(this); + }.bind(this); + this.focusNode.addEvent("keyup", this.keyEvent); + + this.resizeEvent = this.options.constrain ? function(e) { + this._resize(); + }.bind(this) : function() { + this._position(); + }.bind(this); + window.addEvent("resize", this.resizeEvent); + + if(this.options.resetOnScroll) { + this.scrollEvent = function() { + this._position(); + }.bind(this); + window.addEvent("scroll", this.scrollEvent); + } + + return this; + }, + + // Detaches events upon close + _detachEvents: function() { + this.focusNode.removeEvent("keyup", this.keyEvent); + window.removeEvent("resize", this.resizeEvent); + if(this.scrollEvent) window.removeEvent("scroll", this.scrollEvent); + return this; + }, + + // Repositions the box + _position: function() { + var windowSize = window.getSize(), + scrollSize = window.getScroll(), + boxSize = this.box.getSize(); + this.box.setStyles({ + left: scrollSize.x + ((windowSize.x - boxSize.x) / 2), + top: scrollSize.y + ((windowSize.y - boxSize.y) / 2) + }); + this._ie6Size(); + return this; + }, + + // Resizes the box, then positions it + _resize: function() { + var height = this.options.height; + if(height == "auto") { + //get the height of the content box + var max = window.getSize().y - this.options.pad; + if(this.contentBox.getSize().y > max) height = max; + } + this.messageBox.setStyle("height", height); + this._position(); + }, + + // Expose message box + toElement: function () { + return this.messageBox; + }, + + // Expose entire modal box + getBox: function() { + return this.box; + }, + + // Cleanup + destroy: function() { + this._detachEvents(); + this.buttons.each(function(button) { + button.removeEvents("click"); + }); + this.box.dispose(); + delete this.box; + } +}); \ No newline at end of file diff --git a/templates/default/lightface/images/b.png b/templates/default/lightface/images/b.png new file mode 100755 index 0000000000000000000000000000000000000000..f184e6269b343014f58694093b55558dd5dde193 GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0vp^fG6i5I7 literal 0 HcmV?d00001 diff --git a/templates/default/lightface/images/bl.png b/templates/default/lightface/images/bl.png new file mode 100755 index 0000000000000000000000000000000000000000..f6271859d51654b6fb2719df5fe192c8398ecefc GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4IeWS|hDcma=HTLr;y)tIk;ta7 zu_vN6xFht2)0#siNuslamwx#F|G&b~zelu8RJ{}@J*sQsnrJe^+4cq#uT_A>dUgiy Xt!wnnwn>Kojbre1^>bP0l+XkK7z`$3 literal 0 HcmV?d00001 diff --git a/templates/default/lightface/images/br.png b/templates/default/lightface/images/br.png new file mode 100755 index 0000000000000000000000000000000000000000..31f204fc451cd9dd5cfdadfad2d86ed0e1104882 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4IeWS|hDc29J+)AjL4m_HaN4IG z$D^BEUx=k`c-1!J#J_^urVqIKIty1DUX{oyG4)ejx5`MhFFt0jm0SWdC(DBaZ!X9M X+pW>R{=Die&^QK9S3j3^P6&5fKQ0)|NsB1`?eOetk0-h zom{!%#fulOUcP+r;QpgWkDfhy_W9H2=g*&i{qpt6lgA%Fef;q8!`E-$K7ao5fYVE_wU`mefxGy$;zA+ff-AE zV#`)#uL#Io=9^TtCbD=%>DuU$wNbe%g9=uMWi0c_Ulp3SG9;>GMb>ivqBRkj%l%?X zSH_jEDqa&=xH`OR&Y|d%6^T`A5-L~6SFDaKT0Y}YTj{!(h@$1`OT8nDRwP%iz49O} z1L!Ztk|4ie28U-i(ij++v^`xMLn`9lUfh}|!^PlwaCbsl8i%p*;otx3yL3}0t1tD9 zc{BH=Uu{uH` zrjT}>D~hvAPk6~1#kaq?qF7vdB2u;}sy*kb;`7oOvtG{VdpRS|YFRRNR2kj=C%nDw z%Fy;ZYnU!9xADeupj?qp0 zy!(EwYF-Jxw*AVxMJ4{}7X`JSxu$4bdLl@6Q$TypHAU^x6E(7%g4@qrRZK5Ektw?= zw0+L?#4K?5?ZV;3zL!s40ppsXz+|GFiQV##mc8eH9zV|r5<&+Jk2}94*z7Rz69&2E k<)H!tAL)NJeCW#T8SFF!J|2_*FAb9MboFyt=akR{09C4>$N&HU literal 0 HcmV?d00001 diff --git a/templates/default/lightface/images/spinner.gif b/templates/default/lightface/images/spinner.gif new file mode 100755 index 0000000000000000000000000000000000000000..53dd589fa194f5db985e4301c7a73ed4f1b9ad99 GIT binary patch literal 2545 zcma*pX;2hr8VB%~zPqQp=@B)`y1PS96NXtx1_oS2g;AJ+5se@Q;|&UOs2qwM!p4Ca zhJiUal?ExBdstaT~?f3m?cZgNh{frmzMrfcJ8)3 z;P)BJevgQNtw(cfF&90SnBnnr_M&xm@O%6 zzG;!w8(-4o!w_SKEsx_t8i2dHA{$xug+5j_t}~5!rEMsQGpY^uf8#e!ez!6*c<$`# z%2he_t-h<5RT`BLN|GpKWC}6HY^k+5Dom~L=dCyXf!71bXd=Do;)LbM zv+gGZ*&l+=TYuPh^HJ+{Um3v9W8Dqi zZsIDi!j*rKg;1w`oCOc;={#&)!ZF-E-<0u^i2Q3W!xltD%v-T4#;x)CfF(1Ouv)`p z>uUosDu9aCbKX*^H)pSF(Clw+wK}`3LC8Vd5wn~@j##n&8u(Or9|$_$-*q^|8tR^Ze!v`8XRU12fiFxN&Z^HeB_6{>ZFruBWn5U6U_WQI--B zG~o}Ys+mlEb+PC3o7FVVvN+9%m7%O}Y_c3WoYls|wT3|PzzV{wM+0F|$H`%9CT0Y^ za*mZIUl3}$c$+|-?~lU1Lc0B}sn(uvcimZ5#)MR#za@NWsrn^X=yb^$k3_>|w9gkR z>#sW<_Ea0KR$g0dg}fy7K69z6?%wR47d!o@ zGKEctobW(W=VY|Az1{V$NH$hYU5!_r3z~t<1_3#$IhT`+-0m8Lu3WANjbGvNg1vn1 z=E&h$lM^*FtCrg64J+q9{~$Yc&oK@)FJ9|wdu`CBEMKgD->DcQRuxrUW36azX6kqE z(WAIz&9a^4uqfQXZ?4*+k}M^TI=80q9GA}G8+>bb(fyeR`Q-S93>Q-=OX7E#U^f4~jWk3u>uE&r}55)J9D8+*U}{&~xy z+xheT3lqZ$_0RMat&nbp<_ki!f?+E=jik2kBu@#UnX z0AcCVU-1x`*#iFAGPG4?H)8U+YtUJCS?At3FP6 zWjMo76`C~5oga!z%d+xx$x-ljIYP1$9YjNGd0H&jsWL7XsM^)8O;wB>PVnYYXxtSh|$ifidD)RGQ}R zwHJrNIdVjTqdEklY&;>>dZRxDK`F>#B8Ki@pp8f7rwM2mppszvcw{I`Q1%>g09l6Ekr2-S;;<|g2awum zsj!Q?)Pbyi5x+M!F@WFk6jsdhHiry`x0GIzjy;Yj%rtk*Y|Rg9EEqf3wBpTM<@>(0 zk0}s;bd@I0{9$O~|I{qUIC4vLX&EGrCS`%OyAe_n3}9mSiO7}(QE&#Jdx176W`mkS qJBjK|(6Aas*VCO0)PD%QJkkj;=v4CXc=>?~L(zUf4d3(AzhE&|@J-3jTL4n65aGKJa zi>kXgB~~zHg^Zp{CM0mn90WqNOPra}*Z0{kiIv{o~-b=GT*r10o|L fZ$xJ3$neauD_x^sx!G?E&{zgfS3j3^P6f4xp=xbhE&|@J+)AfL4k+GVA`V_ zi@i$(ZI&5dnZ|p1@qvOp9o~Wy8Q7coxO9zA=N|uQu;Ab0z?&8~c9@!JNn1 +***reason*** diff --git a/templates/default/login/act_form.tem b/templates/default/login/act_form.tem new file mode 100755 index 0000000..61a8362 --- /dev/null +++ b/templates/default/login/act_form.tem @@ -0,0 +1,19 @@ +***error*** +
    +

    {L_LOGIN_ACTFORM}

    +
    +
    +

    {L_LOGIN_ACTINTRO}

    +
    +
    + +
    + +
    +
    +
    +
    diff --git a/templates/default/login/email_actcode.tem b/templates/default/login/email_actcode.tem new file mode 100755 index 0000000..187f9cc --- /dev/null +++ b/templates/default/login/email_actcode.tem @@ -0,0 +1,19 @@ +{L_LOGIN_RESEND_GREET}, + +{L_LOGIN_RESEND_INTRO} + +{L_LOGIN_USERNAME}: ***username*** +{L_LOGIN_PASSWORD}: ***password*** + +{L_LOGIN_REG_ACTNEEDED} + +***act_url*** + +{L_LOGIN_REG_ALTACT} + +***act_form*** +***act_code*** + +{L_LOGIN_RESEND_ENJOY} +-- +{L_EMAIL_SIG} diff --git a/templates/default/login/email_lockout.tem b/templates/default/login/email_lockout.tem new file mode 100755 index 0000000..67b6caf --- /dev/null +++ b/templates/default/login/email_lockout.tem @@ -0,0 +1,18 @@ +{L_LOGIN_LOCKOUT_GREETING} ***username***, + +{L_LOGIN_LOCKOUT_MESSAGE} + +{L_LOGIN_USERNAME}: ***username*** +{L_LOGIN_PASSWORD}: ***password*** + +{L_LOGIN_LOCKOUT_ACTNEEDED} + +***act_url*** + +{L_LOGIN_LOCKOUT_ALTACT} + +***act_form*** +***act_code*** + +-- +{L_EMAIL_SIG} diff --git a/templates/default/login/email_recover.tem b/templates/default/login/email_recover.tem new file mode 100755 index 0000000..c2aa48d --- /dev/null +++ b/templates/default/login/email_recover.tem @@ -0,0 +1,9 @@ +{L_LOGIN_RECOVER_GREET}, + +{L_LOGIN_RECOVER_INTRO} + +***reset_url*** + +{L_LOGIN_RECOVER_IGNORE} +-- +{L_EMAIL_SIG} diff --git a/templates/default/login/email_registered.tem b/templates/default/login/email_registered.tem new file mode 100755 index 0000000..b89077e --- /dev/null +++ b/templates/default/login/email_registered.tem @@ -0,0 +1,19 @@ +{L_LOGIN_REG_GREETING}, + +{L_LOGIN_REG_CREATED} + +{L_LOGIN_USERNAME}: ***username*** +{L_LOGIN_PASSWORD}: ***password*** + +{L_LOGIN_REG_ACTNEEDED} + +***act_url*** + +{L_LOGIN_REG_ALTACT} + +***act_form*** +***act_code*** + +{L_LOGIN_REG_ENJOY} +-- +{L_EMAIL_SIG} diff --git a/templates/default/login/email_reset.tem b/templates/default/login/email_reset.tem new file mode 100755 index 0000000..bf6966b --- /dev/null +++ b/templates/default/login/email_reset.tem @@ -0,0 +1,13 @@ +{L_LOGIN_RESET_GREET}, + +{L_LOGIN_RESET_INTRO} + +{L_LOGIN_USERNAME}: ***username*** +{L_LOGIN_PASSWORD}: ***password*** + +{L_LOGIN_RESET_LOGIN} + +***login_url*** + +-- +{L_EMAIL_SIG} diff --git a/templates/default/login/error.tem b/templates/default/login/error.tem new file mode 100755 index 0000000..20eb057 --- /dev/null +++ b/templates/default/login/error.tem @@ -0,0 +1,2 @@ +{L_LOGIN_FAILED}:
    +***reason*** diff --git a/templates/default/login/error_box.tem b/templates/default/login/error_box.tem new file mode 100755 index 0000000..f57cf0d --- /dev/null +++ b/templates/default/login/error_box.tem @@ -0,0 +1,6 @@ + + + + + +
    error***message***
    diff --git a/templates/default/login/failed.tem b/templates/default/login/failed.tem new file mode 100755 index 0000000..2d4687e --- /dev/null +++ b/templates/default/login/failed.tem @@ -0,0 +1,3 @@ +{L_LOGIN_FAILED}:
    +***reason***
    +{L_LOGIN_FAILLIMIT} diff --git a/templates/default/login/force_password.tem b/templates/default/login/force_password.tem new file mode 100755 index 0000000..626e181 --- /dev/null +++ b/templates/default/login/force_password.tem @@ -0,0 +1,39 @@ +***error*** +
    +

    {L_LOGIN_PASSCHANGE}

    +
    +
    +

    ***reason***

    +

    {L_LOGIN_FORCECHANGE_INTRO}

    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +

    {L_LOGIN_POLICY_INTRO}

    +
      +***policy*** +
    +
    + +
    +
    diff --git a/templates/default/login/form.tem b/templates/default/login/form.tem new file mode 100755 index 0000000..f6f04d2 --- /dev/null +++ b/templates/default/login/form.tem @@ -0,0 +1,44 @@ +***error*** +
    +

    {L_LOGIN_LOGINFORM}

    +
    +
    +

    {L_LOGIN_INTRO}

    +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    + ***selfreg*** +
    +
    + + + + + + + diff --git a/templates/default/login/lockedout.tem b/templates/default/login/lockedout.tem new file mode 100755 index 0000000..673598d --- /dev/null +++ b/templates/default/login/lockedout.tem @@ -0,0 +1 @@ +{L_LOGIN_LOCKEDOUT} diff --git a/templates/default/login/login_warn.tem b/templates/default/login/login_warn.tem new file mode 100755 index 0000000..ba52d24 --- /dev/null +++ b/templates/default/login/login_warn.tem @@ -0,0 +1,2 @@ +***warning*** +***message*** diff --git a/templates/default/login/no_selfreg.tem b/templates/default/login/no_selfreg.tem new file mode 100755 index 0000000..e69de29 diff --git a/templates/default/login/page.tem b/templates/default/login/page.tem new file mode 100755 index 0000000..7c38a6d --- /dev/null +++ b/templates/default/login/page.tem @@ -0,0 +1,29 @@ + + + + + {L_LOGIN_TITLE} + + + + + + + + + + + + + + + + ***extrahead*** + + +
    +

    + ***content*** +
    + + diff --git a/templates/default/login/passchange_error.tem b/templates/default/login/passchange_error.tem new file mode 100755 index 0000000..f6d6297 --- /dev/null +++ b/templates/default/login/passchange_error.tem @@ -0,0 +1 @@ +
  • ***error***
  • diff --git a/templates/default/login/passchange_errors.tem b/templates/default/login/passchange_errors.tem new file mode 100755 index 0000000..954ee89 --- /dev/null +++ b/templates/default/login/passchange_errors.tem @@ -0,0 +1,4 @@ +{L_LOGIN_PASSCHANGE_FAILED}: +
      +***errors*** +
    diff --git a/templates/default/login/policy.tem b/templates/default/login/policy.tem new file mode 100755 index 0000000..844a117 --- /dev/null +++ b/templates/default/login/policy.tem @@ -0,0 +1 @@ +
  • ***policy***
  • diff --git a/templates/default/login/recover_error.tem b/templates/default/login/recover_error.tem new file mode 100755 index 0000000..f4180f1 --- /dev/null +++ b/templates/default/login/recover_error.tem @@ -0,0 +1,2 @@ +{L_LOGIN_RECOVER_FAILED}:
    +***reason*** diff --git a/templates/default/login/recover_form.tem b/templates/default/login/recover_form.tem new file mode 100755 index 0000000..517b330 --- /dev/null +++ b/templates/default/login/recover_form.tem @@ -0,0 +1,18 @@ +***error*** +
    +

    {L_LOGIN_RECFORM}

    +
    +
    +

    {L_LOGIN_RECINTRO}

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/templates/default/login/reg_error.tem b/templates/default/login/reg_error.tem new file mode 100755 index 0000000..2b390e6 --- /dev/null +++ b/templates/default/login/reg_error.tem @@ -0,0 +1 @@ +
  • ***reason***
  • diff --git a/templates/default/login/reg_errorlist.tem b/templates/default/login/reg_errorlist.tem new file mode 100755 index 0000000..0210356 --- /dev/null +++ b/templates/default/login/reg_errorlist.tem @@ -0,0 +1,5 @@ +{L_LOGIN_ERR_REGFAILED}: +
      +***errors*** +
    + diff --git a/templates/default/login/resend_error.tem b/templates/default/login/resend_error.tem new file mode 100755 index 0000000..3bb18a1 --- /dev/null +++ b/templates/default/login/resend_error.tem @@ -0,0 +1,2 @@ +{L_LOGIN_RESEND_FAILED}:
    +***reason*** diff --git a/templates/default/login/resend_form.tem b/templates/default/login/resend_form.tem new file mode 100755 index 0000000..dbe6948 --- /dev/null +++ b/templates/default/login/resend_form.tem @@ -0,0 +1,18 @@ +***error*** +
    +

    {L_LOGIN_RESENDFORM}

    +
    +
    +

    {L_LOGIN_RESENDINTRO}

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/templates/default/login/selfreg.js b/templates/default/login/selfreg.js new file mode 100755 index 0000000..9e2b3c4 --- /dev/null +++ b/templates/default/login/selfreg.js @@ -0,0 +1,7 @@ +var regbox; + +function submit_regform() { + regbox.close(); + $('answer').set('value', $('secquest').get('value')); + $('regform').submit(); +} \ No newline at end of file diff --git a/templates/default/login/selfreg.tem b/templates/default/login/selfreg.tem new file mode 100755 index 0000000..504859a --- /dev/null +++ b/templates/default/login/selfreg.tem @@ -0,0 +1,34 @@ +
    + + +

    {L_LOGIN_REG_INTRO}

    +
    +
    + +
    +
    + +
    +
    + + + + +
    +
    +
    diff --git a/templates/default/login/warning_box.tem b/templates/default/login/warning_box.tem new file mode 100755 index 0000000..6471829 --- /dev/null +++ b/templates/default/login/warning_box.tem @@ -0,0 +1,6 @@ + + + + + +
    important***message***
    diff --git a/templates/default/messagebox.tem b/templates/default/messagebox.tem new file mode 100755 index 0000000..43970ef --- /dev/null +++ b/templates/default/messagebox.tem @@ -0,0 +1,13 @@ +
    + ***icon*** +

    ***title***

    +
    +
    +
    ***summary***
    +
    ***longdesc***
    +
    + ***additional*** + ***buttons*** +
    +
    + diff --git a/templates/default/messagebox_button.tem b/templates/default/messagebox_button.tem new file mode 100755 index 0000000..78aa397 --- /dev/null +++ b/templates/default/messagebox_button.tem @@ -0,0 +1 @@ +
    ***message***
    diff --git a/templates/default/messagebox_buttonbar.tem b/templates/default/messagebox_buttonbar.tem new file mode 100755 index 0000000..f41c12d --- /dev/null +++ b/templates/default/messagebox_buttonbar.tem @@ -0,0 +1,3 @@ +
    +***buttons*** +
    diff --git a/templates/default/page.tem b/templates/default/page.tem new file mode 100755 index 0000000..1d48dd3 --- /dev/null +++ b/templates/default/page.tem @@ -0,0 +1,64 @@ + + + + + ***title*** + + + + + + + + + + + + + + + + + + + + + + ***extrahead*** + + + + +
    +***content*** + + +
    +***userbar*** + + diff --git a/templates/default/refreshmeta.tem b/templates/default/refreshmeta.tem new file mode 100755 index 0000000..e11cc91 --- /dev/null +++ b/templates/default/refreshmeta.tem @@ -0,0 +1 @@ + diff --git a/templates/default/userbar/doclink_disabled.tem b/templates/default/userbar/doclink_disabled.tem new file mode 100644 index 0000000..e69de29 diff --git a/templates/default/userbar/doclink_enabled.tem b/templates/default/userbar/doclink_enabled.tem new file mode 100644 index 0000000..b90f5f3 --- /dev/null +++ b/templates/default/userbar/doclink_enabled.tem @@ -0,0 +1 @@ + diff --git a/templates/default/userbar/images/documentation.png b/templates/default/userbar/images/documentation.png new file mode 100755 index 0000000000000000000000000000000000000000..fb3b689e99af38f769c3c72a6957782f617c034b GIT binary patch literal 3642 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 M07*qoM6N<$f^h=b761SM literal 0 HcmV?d00001 diff --git a/templates/default/userbar/images/import.png b/templates/default/userbar/images/import.png new file mode 100755 index 0000000000000000000000000000000000000000..8bdba8f690f810d29e563c4a852bd1b15b863032 GIT binary patch literal 3342 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 literal 0 HcmV?d00001 diff --git a/templates/default/userbar/images/settings.png b/templates/default/userbar/images/settings.png new file mode 100755 index 0000000000000000000000000000000000000000..b6a25e8d5e976f1a6b9dab65c35b7edae879c41f GIT binary patch literal 974 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 diff --git a/templates/default/userbar/profile_loggedin.tem b/templates/default/userbar/profile_loggedin.tem new file mode 100644 index 0000000..19f341c --- /dev/null +++ b/templates/default/userbar/profile_loggedin.tem @@ -0,0 +1,17 @@ + diff --git a/templates/default/userbar/profile_loggedout.tem b/templates/default/userbar/profile_loggedout.tem new file mode 100755 index 0000000..30fc930 --- /dev/null +++ b/templates/default/userbar/profile_loggedout.tem @@ -0,0 +1,15 @@ + diff --git a/templates/default/userbar/profile_loggedout_http.tem b/templates/default/userbar/profile_loggedout_http.tem new file mode 100644 index 0000000..bbc5282 --- /dev/null +++ b/templates/default/userbar/profile_loggedout_http.tem @@ -0,0 +1,3 @@ + diff --git a/templates/default/userbar/profile_loggedout_https.tem b/templates/default/userbar/profile_loggedout_https.tem new file mode 100644 index 0000000..3af6f5d --- /dev/null +++ b/templates/default/userbar/profile_loggedout_https.tem @@ -0,0 +1,16 @@ + diff --git a/templates/default/userbar/userbar.tem b/templates/default/userbar/userbar.tem new file mode 100644 index 0000000..8c516cf --- /dev/null +++ b/templates/default/userbar/userbar.tem @@ -0,0 +1,13 @@ +
    +
    +
      +
    • logo
    • +
    • {V_[sitename]}: ***pagename***
    • +
    +
      +***profile*** +***doclink*** +***import*** +
    +
    +
    diff --git a/templates/default/validator_harness.tem b/templates/default/validator_harness.tem new file mode 100755 index 0000000..5aa4862 --- /dev/null +++ b/templates/default/validator_harness.tem @@ -0,0 +1,10 @@ + + + + + AATL Validation Harness + + +***body*** + +